Compare commits
16 Commits
feat/updat
...
19d0e6d3ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d0e6d3ea | ||
|
|
edfee89bad | ||
|
|
b3db983f62 | ||
|
|
997794eaa5 | ||
|
|
8f70bfbbdb | ||
|
|
3158c35df2 | ||
|
|
bb48d0777b | ||
|
|
24fdc1c17c | ||
|
|
7927df4660 | ||
|
|
e5ceeb2fcc | ||
|
|
e0b0b92a8a | ||
|
|
0d06fac919 | ||
|
|
dc9a1b2b7d | ||
|
|
7e59d5cefa | ||
|
|
9f06a5109f | ||
|
|
b2b2655c1e |
@@ -1,58 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Bug Report"
|
|
||||||
about: "Report a bug or unexpected behavior in the lambda runtime."
|
|
||||||
title: "fix: "
|
|
||||||
ref: "main"
|
|
||||||
assignees: []
|
|
||||||
labels:
|
|
||||||
- bug
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe what you were trying to do when you encountered the bug.
|
|
||||||
Explain what you expected to happen.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe what actually happened.
|
|
||||||
Be specific about the incorrect behavior or error.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Steps to Reproduce
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide step-by-step instructions to reproduce the issue.
|
|
||||||
Include any relevant code, commands, or input.
|
|
||||||
-->
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe what should happen instead.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide relevant information about your environment.
|
|
||||||
-->
|
|
||||||
|
|
||||||
- Lambda version:
|
|
||||||
- Go version:
|
|
||||||
- Operating system:
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Add any other context about the problem.
|
|
||||||
Include error messages, logs, or screenshots if applicable.
|
|
||||||
If none exist, omit this section.
|
|
||||||
-->
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Feature Request"
|
|
||||||
about: "Suggest a new feature or enhancement for the lambda runtime."
|
|
||||||
title: "feat: "
|
|
||||||
ref: "main"
|
|
||||||
assignees: []
|
|
||||||
labels:
|
|
||||||
- enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe the problem or limitation you're encountering.
|
|
||||||
Explain why this feature would be valuable.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Proposed Solution
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe your proposed solution or enhancement.
|
|
||||||
Be specific about what you want to see implemented.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Alternatives Considered
|
|
||||||
|
|
||||||
<!--
|
|
||||||
List any alternative solutions or approaches you've considered.
|
|
||||||
If none exist, omit this section.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!--
|
|
||||||
List clear, testable criteria that define when this feature is complete.
|
|
||||||
Use bullet points starting with •
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Add any other context, screenshots, or examples about the feature request.
|
|
||||||
If none exist, omit this section.
|
|
||||||
-->
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
name: "General Issue"
|
|
||||||
about: "Create an issue that doesn't fit other templates."
|
|
||||||
title: ""
|
|
||||||
ref: "main"
|
|
||||||
assignees: []
|
|
||||||
labels: []
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe the background and context for this issue.
|
|
||||||
Explain why this issue exists.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide a detailed description of what needs to be done.
|
|
||||||
Be clear and specific about the requirements.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!--
|
|
||||||
List clear, testable criteria that define when this issue is complete.
|
|
||||||
Use bullet points starting with •
|
|
||||||
If none exist, omit this section.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Add any other relevant information, links, or references.
|
|
||||||
If none exist, omit this section.
|
|
||||||
-->
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Default Template"
|
|
||||||
about: "The default template for `lambda`."
|
|
||||||
title: "<type>: <description>"
|
|
||||||
ref: "main"
|
|
||||||
assignees: []
|
|
||||||
labels: []
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
First, describe the context for the PR.
|
|
||||||
Then, explain why the PR exists.
|
|
||||||
Finally, in concise, sentence-long bullets, explain each change.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Decisions
|
|
||||||
|
|
||||||
<!--
|
|
||||||
List any major architectural decisions here.
|
|
||||||
If none exist, omit this section.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
<!--
|
|
||||||
List any major benefits here.
|
|
||||||
How would this PR improve the codebase/product?
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Code follows conventional commit format.
|
|
||||||
- [ ] Branch follows naming convention (`<type>/<description>`). Always use underscores.
|
|
||||||
- [ ] Tests pass (if applicable).
|
|
||||||
- [ ] Documentation updated (if applicable).
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,13 +3,18 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
|||||||
156
.golangci.yml
156
.golangci.yml
@@ -1,233 +1,83 @@
|
|||||||
---
|
|
||||||
# golangci-lint configuration file made by @ccoVeille
|
|
||||||
# Source: https://github.com/ccoVeille/golangci-lint-config-examples/
|
|
||||||
# Author: @ccoVeille
|
|
||||||
# License: MIT
|
|
||||||
# Variant: 03-safe
|
|
||||||
# Version: v2.0.0
|
|
||||||
#
|
|
||||||
version: "2"
|
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'
|
||||||
|
|
||||||
# make sure imports are always in a deterministic order
|
gci:
|
||||||
# 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
|
- comments
|
||||||
# These are common false positives in poor code
|
|
||||||
# you should not use this on recent code you write from scratch
|
|
||||||
# More information: https://golangci-lint.run/usage/false-positives/#comments
|
|
||||||
#
|
|
||||||
# Please uncomment the following line if your code is not using the godoc format
|
|
||||||
# - comments
|
|
||||||
|
|
||||||
# Common false positives
|
|
||||||
# feel free to remove this if you don't have any false positives
|
|
||||||
# More information: https://golangci-lint.run/usage/false-positives/#common-false-positives
|
|
||||||
- common-false-positives
|
- 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
|
||||||
|
|
||||||
# Packages should have comments of the form "Package x ...".
|
|
||||||
- name: package-comments
|
|
||||||
|
|
||||||
# context.Context() should be the first parameter of a function when provided as argument.
|
|
||||||
- name: context-as-argument
|
- 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 # filepath, line, and column.
|
- file
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"makefile.configureOnOpen": false
|
||||||
|
}
|
||||||
79
CLAUDE.md
79
CLAUDE.md
@@ -1,12 +1,6 @@
|
|||||||
# Guide To `lambda`
|
# Guide To `lambda`
|
||||||
|
|
||||||
## Documentation Style
|
## Coding Styles
|
||||||
|
|
||||||
Use full sentences.
|
|
||||||
Every sentence gets its own line in Markdown.
|
|
||||||
Every sentence ends in a period.
|
|
||||||
|
|
||||||
## Coding Style
|
|
||||||
|
|
||||||
### Conventional Commits
|
### Conventional Commits
|
||||||
|
|
||||||
@@ -20,13 +14,11 @@ 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**:
|
||||||
|
|
||||||
@@ -35,71 +27,16 @@ 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 --title "<title>" --description "<body>"` - Create a new pull request.
|
- `tea pr create` - 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
|
||||||
|
|
||||||
**Note**: Use `--description` (not `--body`) for PR body content.
|
**Creating PRs**: Always create PRs to the `main` branch unless specified otherwise
|
||||||
|
|
||||||
**Creating PRs**: Always create PRs in a branch other than `main`, to the `main` branch unless specified otherwise. ALWAYS FOLLOW THE PR TEMPLATE, EXACTLY.
|
|
||||||
|
|
||||||
**Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`.
|
|
||||||
This automatically links and closes the issue when the PR is merged.
|
|
||||||
|
|
||||||
### Updating PRs
|
|
||||||
|
|
||||||
When pushing additional changes to an existing PR, add a comment summarizing the new commits.
|
|
||||||
This keeps reviewers informed of what changed since the initial PR description.
|
|
||||||
|
|
||||||
Use the `tea` CLI to add comments to pull requests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tea comment <number> "Comment text"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add a comment to PR #42
|
|
||||||
tea comment 42 "Updated implementation based on feedback"
|
|
||||||
|
|
||||||
# Add a multi-line comment
|
|
||||||
tea comment 42 "Summary of changes:
|
|
||||||
- Fixed bug in reducer
|
|
||||||
- Added new tests"
|
|
||||||
```
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -48,7 +48,7 @@ The "source code" for a work means the preferred form of the work for making mod
|
|||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code runtime used to run it.
|
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
|
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
|
||||||
subprograms and other parts of the work.
|
subprograms and other parts of the work.
|
||||||
|
|||||||
55
Makefile
55
Makefile
@@ -1,55 +1,40 @@
|
|||||||
BINARY_NAME=lambda
|
BINARY_NAME=lambda
|
||||||
TEST=simple
|
TEST=simple
|
||||||
|
|
||||||
.PHONY: help build run profile explain graph docs test bench lint 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 " build - Build the lambda executable"
|
echo " it - Build the lambda binary"
|
||||||
echo " run - Build and run the lambda runtime (use TEST=<name> to specify sample)"
|
echo " run - Build and run with sample input (default: simple.txt)"
|
||||||
echo " profile - Build and run with CPU profiling enabled"
|
echo " profile - Build and run with CPU profiling enabled"
|
||||||
echo " explain - Build and run with explanation mode and profiling"
|
echo " explain - Run with explanation mode and profiling"
|
||||||
echo " graph - Generate and open CPU profile visualization"
|
echo " graph - Generate CPU profile visualization"
|
||||||
echo " docs - Start local godoc server on port 6060"
|
echo " clean - Remove build artifacts"
|
||||||
echo " test - Run tests for all samples"
|
echo ""
|
||||||
echo " bench - Run benchmarks for all samples"
|
echo "Usage: make run TEST=<sample-name>"
|
||||||
echo " lint - Run golangci-lint on all packages"
|
|
||||||
echo " clean - Remove all build artifacts"
|
|
||||||
|
|
||||||
build:
|
it:
|
||||||
go build -o ${BINARY_NAME} ./cmd/lambda
|
go build -o ${BINARY_NAME} ./cmd/lambda
|
||||||
chmod +x ${BINARY_NAME}
|
|
||||||
|
|
||||||
run: build
|
run: it
|
||||||
./${BINARY_NAME} -s -f ./tests/$(TEST).test -o program.out
|
./lambda - < ./samples/$(TEST).txt > program.out
|
||||||
|
|
||||||
profile: build
|
profile: it
|
||||||
./${BINARY_NAME} -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out
|
mkdir -p profile
|
||||||
|
./lambda -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
|
||||||
|
|
||||||
explain: build
|
explain: it
|
||||||
./${BINARY_NAME} -x -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out > explain.out
|
mkdir -p profile
|
||||||
|
./lambda -x -p profile/cpu.prof - < ./samples/$(TEST).txt > program.out
|
||||||
|
|
||||||
graph:
|
graph: profile
|
||||||
go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof
|
go tool pprof -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 ">>> View at 'file://$(PWD)/profile/cpu.svg'"
|
echo "Profile graph saved to 'file://profile/cpu.svg'"
|
||||||
|
|
||||||
docs:
|
|
||||||
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
|
|
||||||
go run golang.org/x/tools/cmd/godoc@latest -http=:6060
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test -v ./cmd/lambda
|
|
||||||
|
|
||||||
bench:
|
|
||||||
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
|
|
||||||
|
|
||||||
lint:
|
|
||||||
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f ${BINARY_NAME}
|
rm -f ${BINARY_NAME} program.out
|
||||||
rm -f program.out
|
|
||||||
rm -rf profile/
|
rm -rf profile/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# lambda
|
# lambda
|
||||||
|
|
||||||
Making a lambda calculus runtime in Go.
|
Making a lambda calculus interpreter in Go.
|
||||||
|
|
||||||
## Things to talk about
|
## Things to talk about
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
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/plugins"
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
|
"git.maximhutz.com/max/lambda/internal/explanation"
|
||||||
|
"git.maximhutz.com/max/lambda/internal/performance"
|
||||||
|
"git.maximhutz.com/max/lambda/internal/statistics"
|
||||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||||
"git.maximhutz.com/max/lambda/pkg/normalorder"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,37 +35,42 @@ 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", compiled.String())
|
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled))
|
||||||
|
|
||||||
// Create reducer with the compiled expression.
|
// Create reduction engine.
|
||||||
runtime := normalorder.NewRuntime(compiled)
|
process := engine.New(options, &compiled)
|
||||||
|
|
||||||
// If the user selected to track CPU performance, attach a profiler.
|
// If the user selected to track CPU performance, attach a profiler to the
|
||||||
|
// process.
|
||||||
if options.Profile != "" {
|
if options.Profile != "" {
|
||||||
plugins.NewPerformance(options.Profile, runtime)
|
profiler := performance.Track(options.Profile)
|
||||||
|
process.On("start", profiler.Start)
|
||||||
|
process.On("end", profiler.End)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user selected to produce a step-by-step explanation, attach an
|
// If the user selected to produce a step-by-step explanation, attach an
|
||||||
// observer.
|
// observer here.
|
||||||
if options.Explanation {
|
if options.Explanation {
|
||||||
plugins.NewExplanation(runtime)
|
explanation.Track(process)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user opted to track statistics, attach a tracker.
|
// If the user opted to track statistics, attach a tracker here, too.
|
||||||
if options.Statistics {
|
if options.Statistics {
|
||||||
plugins.NewStatistics(runtime)
|
statistics := statistics.Track()
|
||||||
|
process.On("start", statistics.Start)
|
||||||
|
process.On("step", statistics.Step)
|
||||||
|
process.On("end", statistics.End)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||||
if options.Verbose {
|
if options.Verbose {
|
||||||
plugins.NewLogs(logger, runtime)
|
process.On("step", func() {
|
||||||
|
logger.Info("reduction", "tree", lambda.Stringify(compiled))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run reduction.
|
process.Run()
|
||||||
runtime.Run()
|
|
||||||
|
|
||||||
// Return the final reduced result.
|
// Return the final reduced result.
|
||||||
result := runtime.Expression().String()
|
fmt.Println(lambda.Stringify(compiled))
|
||||||
err = options.Destination.Write(result)
|
|
||||||
cli.HandleError(err)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/normalorder"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Helper function to run a single sample through the lambda runtime.
|
|
||||||
func runSample(samplePath string) (string, error) {
|
|
||||||
// Read the sample file.
|
|
||||||
input, err := os.ReadFile(samplePath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse code into syntax tree.
|
|
||||||
ast, err := saccharine.Parse(string(input))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile expression to lambda calculus.
|
|
||||||
compiled := convert.SaccharineToLambda(ast)
|
|
||||||
|
|
||||||
// Create and run the reducer.
|
|
||||||
reducer := normalorder.NewRuntime(compiled)
|
|
||||||
reducer.Run()
|
|
||||||
|
|
||||||
return reducer.Expression().String() + "\n", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that all samples produce expected output.
|
|
||||||
func TestSamplesValidity(t *testing.T) {
|
|
||||||
// Discover all .test files in the tests directory.
|
|
||||||
testFiles, err := filepath.Glob("../../tests/*.test")
|
|
||||||
assert.NoError(t, err, "Failed to read tests directory.")
|
|
||||||
assert.NotEmpty(t, testFiles, "No '*.test' files found in directory.")
|
|
||||||
|
|
||||||
for _, testPath := range testFiles {
|
|
||||||
// Build expected file path.
|
|
||||||
expectedPath := strings.TrimSuffix(testPath, filepath.Ext(testPath)) + ".expected"
|
|
||||||
|
|
||||||
name := strings.TrimSuffix(filepath.Base(testPath), filepath.Ext(testPath))
|
|
||||||
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
// Run the sample and capture output.
|
|
||||||
actual, err := runSample(testPath)
|
|
||||||
assert.NoError(t, err, "Failed to run sample.")
|
|
||||||
|
|
||||||
// Read expected output.
|
|
||||||
expectedBytes, err := os.ReadFile(expectedPath)
|
|
||||||
assert.NoError(t, err, "Failed to read expected output.")
|
|
||||||
expected := string(expectedBytes)
|
|
||||||
|
|
||||||
// Compare outputs.
|
|
||||||
assert.Equal(t, expected, actual, "Output does not match expected.")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark all samples using sub-benchmarks.
|
|
||||||
func BenchmarkSamples(b *testing.B) {
|
|
||||||
// Discover all .test files in the tests directory.
|
|
||||||
testFiles, err := filepath.Glob("../../tests/*.test")
|
|
||||||
assert.NoError(b, err, "Failed to read tests directory.")
|
|
||||||
assert.NotEmpty(b, testFiles, "No '*.test' files found in directory.")
|
|
||||||
|
|
||||||
for _, path := range testFiles {
|
|
||||||
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
|
||||||
|
|
||||||
b.Run(name, func(b *testing.B) {
|
|
||||||
for b.Loop() {
|
|
||||||
_, err := runSample(path)
|
|
||||||
assert.NoError(b, err, "Failed to run sample.")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.maximhutz.com/max/lambda/internal/cli"
|
|
||||||
"git.maximhutz.com/max/lambda/internal/registry"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/engine/normalorder"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MakeRegistry() *registry.Registry {
|
|
||||||
r := registry.New()
|
|
||||||
|
|
||||||
// Codecs
|
|
||||||
r.AddCodec(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda"))
|
|
||||||
|
|
||||||
// Engines
|
|
||||||
r.AddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda"))
|
|
||||||
|
|
||||||
// Marshalers
|
|
||||||
r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda"))
|
|
||||||
r.AddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine"))
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
8
go.mod
8
go.mod
@@ -1,11 +1,3 @@
|
|||||||
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
9
go.sum
@@ -1,9 +0,0 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Codec interface {
|
|
||||||
codec.Codec[Repr, Repr]
|
|
||||||
|
|
||||||
InType() string
|
|
||||||
OutType() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type convertedCodec[T, U any] struct {
|
|
||||||
codec codec.Codec[T, U]
|
|
||||||
inType, outType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c convertedCodec[T, U]) Decode(r Repr) (Repr, error) {
|
|
||||||
u, ok := r.Data().(U)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType)
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := c.codec.Decode(u)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRepr(c.outType, t), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c convertedCodec[T, U]) Encode(r Repr) (Repr, error) {
|
|
||||||
t, ok := r.Data().(T)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.outType)
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := c.codec.Encode(t)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRepr(c.inType, u), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c convertedCodec[T, U]) InType() string { return c.inType }
|
|
||||||
|
|
||||||
func (c convertedCodec[T, U]) OutType() string { return c.outType }
|
|
||||||
|
|
||||||
func ConvertCodec[T, U any](e codec.Codec[T, U], inType, outType string) Codec {
|
|
||||||
return convertedCodec[T, U]{e, inType, outType}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/engine"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Engine interface {
|
|
||||||
engine.Engine[Repr]
|
|
||||||
|
|
||||||
Name() string
|
|
||||||
InType() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type convertedEngine[T any] struct {
|
|
||||||
engine engine.Engine[T]
|
|
||||||
name string
|
|
||||||
inType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b convertedEngine[T]) InType() string { return b.inType }
|
|
||||||
|
|
||||||
func (b convertedEngine[T]) Name() string { return b.name }
|
|
||||||
|
|
||||||
func (b convertedEngine[T]) Get() (Repr, error) {
|
|
||||||
s, err := b.engine.Get()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRepr(b.inType, s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b convertedEngine[T]) Set(r Repr) error {
|
|
||||||
if t, ok := r.Data().(T); ok {
|
|
||||||
return b.engine.Set(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Incorrent format '%s' for engine '%s'.", r.Id(), b.inType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b convertedEngine[T]) Step(i int) bool {
|
|
||||||
return b.engine.Step(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConvertEngine[T any](e engine.Engine[T], name string, inType string) Engine {
|
|
||||||
return convertedEngine[T]{e, name, inType}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Marshaler interface {
|
|
||||||
codec.Marshaler[Repr]
|
|
||||||
|
|
||||||
InType() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type convertedMarshaler[T any] struct {
|
|
||||||
codec codec.Marshaler[T]
|
|
||||||
inType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c convertedMarshaler[T]) Decode(s string) (Repr, error) {
|
|
||||||
t, err := c.codec.Decode(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRepr(c.inType, t), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c convertedMarshaler[T]) Encode(r Repr) (string, error) {
|
|
||||||
t, ok := r.Data().(T)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("could not parse '%v' as 'string'", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.codec.Encode(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c convertedMarshaler[T]) InType() string { return c.inType }
|
|
||||||
|
|
||||||
func ConvertMarshaler[T any](e codec.Marshaler[T], inType string) Marshaler {
|
|
||||||
return convertedMarshaler[T]{e, inType}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
type Repr interface {
|
|
||||||
// Id returns to name of the objects underlying representation. If is
|
|
||||||
// assumed that if two Repr objects have the same Id(), they share the same
|
|
||||||
// representation.
|
|
||||||
Id() string
|
|
||||||
|
|
||||||
Data() any
|
|
||||||
}
|
|
||||||
|
|
||||||
type baseRepr struct {
|
|
||||||
id string
|
|
||||||
data any
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r baseRepr) Id() string { return r.id }
|
|
||||||
|
|
||||||
func (r baseRepr) Data() any { return r.data }
|
|
||||||
|
|
||||||
func NewRepr(id string, data any) Repr { return baseRepr{id, data} }
|
|
||||||
@@ -4,7 +4,6 @@ package config
|
|||||||
// 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.
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A method of writing output to the user.
|
|
||||||
type Destination interface {
|
|
||||||
// Write data to this destination.
|
|
||||||
Write(data string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// A destination writing to stdout.
|
|
||||||
type StdoutDestination struct{}
|
|
||||||
|
|
||||||
func (d StdoutDestination) Write(data string) error {
|
|
||||||
fmt.Println(data)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// A destination writing to a file.
|
|
||||||
type FileDestination struct{ Path string }
|
|
||||||
|
|
||||||
func (d FileDestination) Write(data string) error {
|
|
||||||
return os.WriteFile(d.Path, []byte(data+"\n"), 0644)
|
|
||||||
}
|
|
||||||
@@ -12,42 +12,25 @@ 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).")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Parse source type.
|
// There must only be one input argument.
|
||||||
var source Source
|
if flag.NArg() == 0 {
|
||||||
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")
|
return nil, fmt.Errorf("no input given")
|
||||||
} else if flag.NArg() > 1 {
|
} else if flag.NArg() > 1 {
|
||||||
return nil, fmt.Errorf("more than 1 command-line argument")
|
return nil, fmt.Errorf("more than 1 command-line argument")
|
||||||
} else {
|
}
|
||||||
// Positional argument.
|
|
||||||
|
// Parse source type.
|
||||||
|
var source Source
|
||||||
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,
|
||||||
|
|||||||
@@ -27,15 +27,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
32
internal/engine/engine.go
Normal file
32
internal/engine/engine.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Package "engine" provides an extensible interface for users to interfact with
|
||||||
|
// λ-calculus.
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A process for reducing one λ-expression.
|
||||||
|
type Engine struct {
|
||||||
|
Config *config.Config
|
||||||
|
Expression *lambda.Expression
|
||||||
|
emitter.Emitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new engine, given an unreduced λ-expression.
|
||||||
|
func New(config *config.Config, expression *lambda.Expression) *Engine {
|
||||||
|
return &Engine{Config: config, Expression: expression}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin the reduction process.
|
||||||
|
func (e Engine) Run() {
|
||||||
|
e.Emit("start")
|
||||||
|
|
||||||
|
for lambda.ReduceOnce(e.Expression) {
|
||||||
|
e.Emit("step")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Emit("end")
|
||||||
|
}
|
||||||
32
internal/explanation/tracker.go
Normal file
32
internal/explanation/tracker.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Package "explanation" provides a observer to gather the reasoning during the
|
||||||
|
// reduction, and present a thorough explanation to the user for each step.
|
||||||
|
package explanation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track the reductions made by a reduction proess.
|
||||||
|
type Tracker struct {
|
||||||
|
process *engine.Engine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches a new explanation tracker to a process.
|
||||||
|
func Track(process *engine.Engine) *Tracker {
|
||||||
|
tracker := &Tracker{process: process}
|
||||||
|
process.On("start", tracker.Start)
|
||||||
|
process.On("step", tracker.Step)
|
||||||
|
|
||||||
|
return tracker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Start() {
|
||||||
|
fmt.Println(lambda.Stringify(*t.process.Expression))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Step() {
|
||||||
|
fmt.Println(" =", lambda.Stringify(*t.process.Expression))
|
||||||
|
}
|
||||||
53
internal/performance/tracker.go
Normal file
53
internal/performance/tracker.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Package "performance" provides a tracker to observer CPU performance during
|
||||||
|
// execution.
|
||||||
|
package performance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime/pprof"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Observes a reduction process, and publishes a CPU performance profile on
|
||||||
|
// completion.
|
||||||
|
type Tracker struct {
|
||||||
|
File string
|
||||||
|
filePointer *os.File
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a performance tracker that outputs a profile to "file".
|
||||||
|
func Track(file string) *Tracker {
|
||||||
|
return &Tracker{File: file}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin profiling.
|
||||||
|
func (t *Tracker) Start() {
|
||||||
|
var absPath string
|
||||||
|
|
||||||
|
absPath, t.Error = filepath.Abs(t.File)
|
||||||
|
if t.Error != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Error = os.MkdirAll(filepath.Dir(absPath), 0777)
|
||||||
|
if t.Error != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.filePointer, t.Error = os.Create(absPath)
|
||||||
|
if t.Error != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Error = pprof.StartCPUProfile(t.filePointer)
|
||||||
|
if t.Error != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop profiling.
|
||||||
|
func (t *Tracker) End() {
|
||||||
|
pprof.StopCPUProfile()
|
||||||
|
t.filePointer.Close()
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Registry struct {
|
|
||||||
marshalers map[string]cli.Marshaler
|
|
||||||
codecs []cli.Codec
|
|
||||||
engines map[string]cli.Engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *Registry {
|
|
||||||
return &Registry{
|
|
||||||
marshalers: map[string]cli.Marshaler{},
|
|
||||||
codecs: []cli.Codec{},
|
|
||||||
engines: map[string]cli.Engine{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) AddCodec(c cli.Codec) error {
|
|
||||||
r.codecs = append(r.codecs, c)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) AddMarshaler(c cli.Marshaler) error {
|
|
||||||
if _, ok := r.marshalers[c.InType()]; ok {
|
|
||||||
return fmt.Errorf("marshaler for '%s' already registered", c.InType())
|
|
||||||
}
|
|
||||||
|
|
||||||
r.marshalers[c.InType()] = c
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) AddEngine(e cli.Engine) error {
|
|
||||||
if _, ok := r.engines[e.Name()]; ok {
|
|
||||||
return fmt.Errorf("engine '%s' already registered", e.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
r.engines[e.Name()] = e
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) GetEngine(name string) (cli.Engine, error) {
|
|
||||||
e, ok := r.engines[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("engine '%s' not found", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) {
|
|
||||||
panic("")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) Marshal(repr cli.Repr) (string, error) {
|
|
||||||
m, ok := r.marshalers[repr.Id()]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("no marshaler for '%s'", repr.Id())
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.Encode(repr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) Unmarshal(s string, outType string) (cli.Repr, error) {
|
|
||||||
m, ok := r.marshalers[outType]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("no marshaler for '%s'", outType)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.Decode(s)
|
|
||||||
}
|
|
||||||
28
internal/statistics/statistics.go
Normal file
28
internal/statistics/statistics.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Package "statistics" provides a way to observer reduction speed during
|
||||||
|
// execution.
|
||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Statistics for a specific reduction.
|
||||||
|
type Results struct {
|
||||||
|
StepsTaken uint64 // Number of steps taken during execution.
|
||||||
|
TimeElapsed uint64 // The time (ms) taken for execution to complete.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the average number of operations per second of the execution.
|
||||||
|
func (r Results) OpsPerSecond() float32 {
|
||||||
|
return float32(r.StepsTaken) / (float32(r.TimeElapsed) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the results as a string.
|
||||||
|
func (r Results) String() string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
fmt.Fprintln(&builder, "Time Spent:", r.TimeElapsed, "ms")
|
||||||
|
fmt.Fprintln(&builder, "Steps:", r.StepsTaken)
|
||||||
|
fmt.Fprintln(&builder, "Speed:", r.OpsPerSecond(), "ops")
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
36
internal/statistics/tracker.go
Normal file
36
internal/statistics/tracker.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package statistics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An observer, to track reduction performance.
|
||||||
|
type Tracker struct {
|
||||||
|
start time.Time
|
||||||
|
steps uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new reduction performance tracker.
|
||||||
|
func Track() *Tracker {
|
||||||
|
return &Tracker{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Start() {
|
||||||
|
t.start = time.Now()
|
||||||
|
t.steps = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) Step() {
|
||||||
|
t.steps++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracker) End() {
|
||||||
|
results := Results{
|
||||||
|
StepsTaken: t.steps,
|
||||||
|
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(os.Stderr, results.String())
|
||||||
|
}
|
||||||
114
makefile-improvements.md
Normal file
114
makefile-improvements.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# 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.
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package codec
|
|
||||||
|
|
||||||
type Codec[T, U any] interface {
|
|
||||||
Encode(T) (U, error)
|
|
||||||
Decode(U) (T, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Marshaler[T any] = Codec[T, string]
|
|
||||||
@@ -3,24 +3,23 @@ package convert
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeAtom(n *saccharine.Atom) lambda.Expression {
|
func convertAtom(n *saccharine.Atom) lambda.Expression {
|
||||||
return lambda.NewVariable(n.Name)
|
return lambda.NewVariable(n.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||||
result := encodeExpression(n.Body)
|
result := SaccharineToLambda(n.Body)
|
||||||
|
|
||||||
parameters := n.Parameters
|
parameters := n.Parameters
|
||||||
|
|
||||||
// If the function has no parameters, it is a thunk. Lambda calculus still
|
// If the function has no parameters, it is a thunk. Lambda calculus still
|
||||||
// requires _some_ parameter exists, so generate one.
|
// requires _some_ parameter exists, so generate one.
|
||||||
if len(parameters) == 0 {
|
if len(parameters) == 0 {
|
||||||
freeVars := result.GetFree()
|
freeVars := lambda.GetFreeVariables(result)
|
||||||
freshName := lambda.GenerateFreshName(freeVars)
|
freshName := lambda.GenerateFreshName(freeVars)
|
||||||
parameters = append(parameters, freshName)
|
parameters = append(parameters, freshName)
|
||||||
}
|
}
|
||||||
@@ -32,13 +31,13 @@ func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeApplication(n *saccharine.Application) lambda.Expression {
|
func convertApplication(n *saccharine.Application) lambda.Expression {
|
||||||
result := encodeExpression(n.Abstraction)
|
result := SaccharineToLambda(n.Abstraction)
|
||||||
|
|
||||||
arguments := []lambda.Expression{}
|
arguments := []lambda.Expression{}
|
||||||
for _, argument := range n.Arguments {
|
for _, argument := range n.Arguments {
|
||||||
encodeedArgument := encodeExpression(argument)
|
convertedArgument := SaccharineToLambda(argument)
|
||||||
arguments = append(arguments, encodeedArgument)
|
arguments = append(arguments, convertedArgument)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, argument := range arguments {
|
for _, argument := range arguments {
|
||||||
@@ -52,9 +51,9 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
|
|||||||
var value lambda.Expression
|
var value lambda.Expression
|
||||||
|
|
||||||
if len(s.Parameters) == 0 {
|
if len(s.Parameters) == 0 {
|
||||||
value = encodeExpression(s.Body)
|
value = SaccharineToLambda(s.Body)
|
||||||
} else {
|
} else {
|
||||||
value = encodeAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
|
value = convertAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
return lambda.NewApplication(
|
return lambda.NewApplication(
|
||||||
@@ -64,11 +63,11 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
|
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
|
||||||
freshVar := lambda.GenerateFreshName(e.GetFree())
|
freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
|
||||||
|
|
||||||
return lambda.NewApplication(
|
return lambda.NewApplication(
|
||||||
lambda.NewAbstraction(freshVar, e),
|
lambda.NewAbstraction(freshVar, e),
|
||||||
encodeExpression(s.Value),
|
SaccharineToLambda(s.Value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +82,8 @@ func reduceStatement(s saccharine.Statement, e lambda.Expression) lambda.Express
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeClause(n *saccharine.Clause) lambda.Expression {
|
func convertClause(n *saccharine.Clause) lambda.Expression {
|
||||||
result := encodeExpression(n.Returns)
|
result := SaccharineToLambda(n.Returns)
|
||||||
|
|
||||||
for i := len(n.Statements) - 1; i >= 0; i-- {
|
for i := len(n.Statements) - 1; i >= 0; i-- {
|
||||||
result = reduceStatement(n.Statements[i], result)
|
result = reduceStatement(n.Statements[i], result)
|
||||||
@@ -93,46 +92,17 @@ func encodeClause(n *saccharine.Clause) lambda.Expression {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeExpression(s saccharine.Expression) lambda.Expression {
|
func SaccharineToLambda(n saccharine.Expression) lambda.Expression {
|
||||||
switch s := s.(type) {
|
switch n := n.(type) {
|
||||||
case *saccharine.Atom:
|
case *saccharine.Atom:
|
||||||
return encodeAtom(s)
|
return convertAtom(n)
|
||||||
case *saccharine.Abstraction:
|
case *saccharine.Abstraction:
|
||||||
return encodeAbstraction(s)
|
return convertAbstraction(n)
|
||||||
case *saccharine.Application:
|
case *saccharine.Application:
|
||||||
return encodeApplication(s)
|
return convertApplication(n)
|
||||||
case *saccharine.Clause:
|
case *saccharine.Clause:
|
||||||
return encodeClause(s)
|
return convertClause(n)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Errorf("unknown expression type: %T", s))
|
panic(fmt.Errorf("unknown expression type: %T", n))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeExression(l lambda.Expression) saccharine.Expression {
|
|
||||||
switch l := l.(type) {
|
|
||||||
case lambda.Variable:
|
|
||||||
return saccharine.NewAtom(l.Name())
|
|
||||||
case lambda.Abstraction:
|
|
||||||
return saccharine.NewAbstraction(
|
|
||||||
[]string{l.Parameter()},
|
|
||||||
decodeExression(l.Body()))
|
|
||||||
case lambda.Application:
|
|
||||||
return saccharine.NewApplication(
|
|
||||||
decodeExression(l.Abstraction()),
|
|
||||||
[]saccharine.Expression{decodeExression(l.Argument())})
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unknown expression type: %T", l))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Saccharine2Lambda struct{}
|
|
||||||
|
|
||||||
func (c Saccharine2Lambda) Decode(l lambda.Expression) (saccharine.Expression, error) {
|
|
||||||
return decodeExression(l), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Saccharine2Lambda) Encode(s saccharine.Expression) (lambda.Expression, error) {
|
|
||||||
return encodeExpression(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ codec.Codec[saccharine.Expression, lambda.Expression] = (*Saccharine2Lambda)(nil)
|
|
||||||
|
|||||||
6
pkg/deltanet/deltanet.go
Normal file
6
pkg/deltanet/deltanet.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Package "deltanet" is a reduction strategy using ∆-nets.
|
||||||
|
package deltanet
|
||||||
|
|
||||||
|
type Graph struct {
|
||||||
|
Nodes []Node
|
||||||
|
}
|
||||||
80
pkg/deltanet/node.go
Normal file
80
pkg/deltanet/node.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package deltanet
|
||||||
|
|
||||||
|
// A connection between exactly two nodes in a graph.
|
||||||
|
type Edge struct {
|
||||||
|
A, B Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all nodes the edge is connected to.
|
||||||
|
func (e Edge) GetConnections() []Node { return []Node{e.A, e.B} }
|
||||||
|
|
||||||
|
// Determines if a node is connected via this edge.
|
||||||
|
func (e Edge) IsConnected(n Node) bool { return e.A == n || e.B == n }
|
||||||
|
|
||||||
|
// Swaps an edges connected with one node, for another.
|
||||||
|
func (e *Edge) Swap(from Node, to Node) {
|
||||||
|
if e.A == from {
|
||||||
|
e.A = to
|
||||||
|
}
|
||||||
|
if e.B == from {
|
||||||
|
e.B = to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the edge is connected to each node via their pricniple ports.
|
||||||
|
func (e Edge) IsPrincipleEdge() bool {
|
||||||
|
return e.A.GetMainPort() == e && e.B.GetMainPort() == e
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node interface {
|
||||||
|
// Returns the principle port that the node is attached to.
|
||||||
|
GetMainPort() Edge
|
||||||
|
|
||||||
|
// Returns all auxiliary ports that the node has. These ports are guaranteed
|
||||||
|
// to be ordered clockwise, as they would appear graphically.
|
||||||
|
GetAuxPorts() []Edge
|
||||||
|
|
||||||
|
// Returns the label of the node. May be blank.
|
||||||
|
GetLabel() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EraserNode struct {
|
||||||
|
Main Edge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n EraserNode) GetLabel() string { return "Ⓧ" }
|
||||||
|
func (n EraserNode) GetMainPort() Edge { return n.Main }
|
||||||
|
func (n EraserNode) GetAuxPorts() []Edge { return []Edge{} }
|
||||||
|
|
||||||
|
type ReplicatorNode struct {
|
||||||
|
Main Edge
|
||||||
|
Level uint
|
||||||
|
Aux []Edge
|
||||||
|
Deltas []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n ReplicatorNode) GetLabel() string { return "" }
|
||||||
|
func (n ReplicatorNode) GetMainPort() Edge { return n.Main }
|
||||||
|
func (n ReplicatorNode) GetAuxPorts() []Edge { return n.Aux }
|
||||||
|
|
||||||
|
// Returns the level of the replicator node.
|
||||||
|
func (n ReplicatorNode) GetLevel() uint { return n.Level }
|
||||||
|
|
||||||
|
type FanNode struct {
|
||||||
|
Label string
|
||||||
|
Main Edge
|
||||||
|
Left, Right Edge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n FanNode) GetLabel() string { return n.Label }
|
||||||
|
func (n FanNode) GetMainPort() Edge { return n.Main }
|
||||||
|
func (n FanNode) GetAuxPorts() []Edge { return []Edge{n.Left, n.Right} }
|
||||||
|
|
||||||
|
type TerminalNode struct {
|
||||||
|
Label string
|
||||||
|
Main Edge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n TerminalNode) GetLabel() string { return n.Label }
|
||||||
|
func (n TerminalNode) GetMainPort() Edge { return n.Main }
|
||||||
|
func (n TerminalNode) GetAuxPorts() []Edge { return []Edge{} }
|
||||||
@@ -2,45 +2,53 @@ package emitter
|
|||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
|
||||||
type Emitter[E comparable] interface {
|
type Observer struct {
|
||||||
On(E, func()) Listener[E]
|
fn func()
|
||||||
Off(Listener[E])
|
message string
|
||||||
Emit(E)
|
emitter *Emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseEmitter[E comparable] struct {
|
type Emitter struct {
|
||||||
listeners map[E]set.Set[Listener[E]]
|
listeners map[string]*set.Set[*Observer]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
|
func Ignore[T any](fn func()) func(T) {
|
||||||
if e.listeners[kind] == nil {
|
return func(T) { fn() }
|
||||||
e.listeners[kind] = set.New[Listener[E]]()
|
}
|
||||||
|
|
||||||
|
func (e *Emitter) On(message string, fn func()) *Observer {
|
||||||
|
observer := &Observer{
|
||||||
|
fn: fn,
|
||||||
|
message: message,
|
||||||
|
emitter: e,
|
||||||
}
|
}
|
||||||
|
|
||||||
listener := &BaseListener[E]{kind, fn}
|
if e.listeners == nil {
|
||||||
e.listeners[kind].Add(listener)
|
e.listeners = map[string]*set.Set[*Observer]{}
|
||||||
return listener
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BaseEmitter[E]) Off(listener Listener[E]) {
|
|
||||||
kind := listener.Kind()
|
|
||||||
if e.listeners[kind] != nil {
|
|
||||||
e.listeners[kind].Remove(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BaseEmitter[E]) Emit(event E) {
|
|
||||||
if e.listeners[event] == nil {
|
|
||||||
e.listeners[event] = set.New[Listener[E]]()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for listener := range e.listeners[event].Items() {
|
if e.listeners[message] == nil {
|
||||||
listener.Run()
|
e.listeners[message] = set.New[*Observer]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.listeners[message].Add(observer)
|
||||||
|
return observer
|
||||||
}
|
}
|
||||||
|
|
||||||
func New[E comparable]() *BaseEmitter[E] {
|
func (o *Observer) Off() {
|
||||||
return &BaseEmitter[E]{
|
if o.emitter.listeners[o.message] == nil {
|
||||||
listeners: map[E]set.Set[Listener[E]]{},
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
o.emitter.listeners[o.message].Remove(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Emitter) Emit(message string) {
|
||||||
|
if e.listeners[message] == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for listener := range *e.listeners[message] {
|
||||||
|
listener.fn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package emitter
|
|
||||||
|
|
||||||
type Listener[E comparable] interface {
|
|
||||||
Kind() E
|
|
||||||
Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseListener[E comparable] struct {
|
|
||||||
kind E
|
|
||||||
fn func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l BaseListener[E]) Kind() E {
|
|
||||||
return l.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l BaseListener[E]) Run() {
|
|
||||||
l.fn()
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package engine
|
|
||||||
|
|
||||||
type Engine[T any] interface {
|
|
||||||
Get() (T, error)
|
|
||||||
Set(T) error
|
|
||||||
Step(int) bool
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package normalorder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/engine"
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Engine struct {
|
|
||||||
expr lambda.Expression
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Engine) Get() (lambda.Expression, error) {
|
|
||||||
return e.expr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Engine) Set(l lambda.Expression) error {
|
|
||||||
e.expr = l
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Engine) Step(i int) bool {
|
|
||||||
var reduced bool
|
|
||||||
|
|
||||||
for range i {
|
|
||||||
e.expr, reduced = ReduceOnce(e.expr)
|
|
||||||
if !reduced {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ engine.Engine[lambda.Expression] = (*Engine)(nil)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package normalorder
|
|
||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/lambda"
|
|
||||||
|
|
||||||
func ReduceOnce(e lambda.Expression) (lambda.Expression, bool) {
|
|
||||||
switch e := e.(type) {
|
|
||||||
case lambda.Abstraction:
|
|
||||||
body, reduced := ReduceOnce(e.Body())
|
|
||||||
if reduced {
|
|
||||||
return lambda.NewAbstraction(e.Parameter(), body), true
|
|
||||||
}
|
|
||||||
return e, false
|
|
||||||
|
|
||||||
case lambda.Application:
|
|
||||||
if fn, fnOk := e.Abstraction().(lambda.Abstraction); fnOk {
|
|
||||||
return fn.Body().Substitute(fn.Parameter(), e.Argument()), true
|
|
||||||
}
|
|
||||||
|
|
||||||
abs, reduced := ReduceOnce(e.Abstraction())
|
|
||||||
if reduced {
|
|
||||||
return lambda.NewApplication(abs, e.Argument()), true
|
|
||||||
}
|
|
||||||
|
|
||||||
arg, reduced := ReduceOnce(e.Argument())
|
|
||||||
if reduced {
|
|
||||||
return lambda.NewApplication(e.Abstraction(), arg), true
|
|
||||||
}
|
|
||||||
|
|
||||||
return e, false
|
|
||||||
|
|
||||||
default:
|
|
||||||
return e, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
pkg/fifo/fifo.go
Normal file
35
pkg/fifo/fifo.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package fifo
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type FIFO[T any] []T
|
||||||
|
|
||||||
|
func New[T any](items ...T) *FIFO[T] {
|
||||||
|
f := FIFO[T](items)
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FIFO[T]) Push(item T) {
|
||||||
|
*f = append(*f, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FIFO[T]) Empty() bool {
|
||||||
|
return len(*f) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FIFO[T]) MustPop() T {
|
||||||
|
var item T
|
||||||
|
|
||||||
|
*f, item = (*f)[:len(*f)-1], (*f)[len(*f)-1]
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FIFO[T]) Pop() (T, error) {
|
||||||
|
var item T
|
||||||
|
|
||||||
|
if f.Empty() {
|
||||||
|
return item, fmt.Errorf("stack is exhausted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.MustPop(), nil
|
||||||
|
}
|
||||||
@@ -1,100 +1,62 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/set"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Expression is the interface for all lambda calculus expression types.
|
|
||||||
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
|
||||||
type Expression interface {
|
type Expression interface {
|
||||||
fmt.Stringer
|
Accept(Visitor)
|
||||||
|
Copy() Expression
|
||||||
// Substitute replaces all free occurrences of the target variable with the
|
|
||||||
// replacement expression. Alpha-renaming is performed automatically to
|
|
||||||
// avoid variable capture.
|
|
||||||
Substitute(target string, replacement Expression) Expression
|
|
||||||
|
|
||||||
// GetFree returns the set of all free variable names in the expression.
|
|
||||||
// This function does not mutate the input expression.
|
|
||||||
// The returned set is newly allocated and can be modified by the caller.
|
|
||||||
GetFree() set.Set[string]
|
|
||||||
|
|
||||||
// Rename replaces all occurrences of the target variable name with the new name.
|
|
||||||
Rename(target string, newName string) Expression
|
|
||||||
|
|
||||||
// IsFree returns true if the variable name n occurs free in the expression.
|
|
||||||
// This function does not mutate the input expression.
|
|
||||||
IsFree(n string) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
type Abstraction struct {
|
type Abstraction struct {
|
||||||
parameter string
|
Parameter string
|
||||||
body Expression
|
Body Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Expression = Abstraction{}
|
func (a *Abstraction) Copy() Expression {
|
||||||
|
return NewAbstraction(a.Parameter, a.Body.Copy())
|
||||||
func (a Abstraction) Parameter() string {
|
|
||||||
return a.parameter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Abstraction) Body() Expression {
|
func (a *Abstraction) Accept(v Visitor) {
|
||||||
return a.body
|
v.VisitAbstraction(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Abstraction) String() string {
|
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
||||||
return "\\" + a.parameter + "." + a.body.String()
|
return &Abstraction{Parameter: parameter, Body: body}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAbstraction(parameter string, body Expression) Abstraction {
|
|
||||||
return Abstraction{parameter, body}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
abstraction Expression
|
Abstraction Expression
|
||||||
argument Expression
|
Argument Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Expression = Application{}
|
func (a *Application) Copy() Expression {
|
||||||
|
return NewApplication(a.Abstraction.Copy(), a.Argument.Copy())
|
||||||
func (a Application) Abstraction() Expression {
|
|
||||||
return a.abstraction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Application) Argument() Expression {
|
func (a *Application) Accept(v Visitor) {
|
||||||
return a.argument
|
v.VisitApplication(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Application) String() string {
|
func NewApplication(function Expression, argument Expression) *Application {
|
||||||
return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
|
return &Application{Abstraction: function, Argument: argument}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApplication(abstraction Expression, argument Expression) Application {
|
|
||||||
return Application{abstraction, argument}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
type Variable struct {
|
type Variable struct {
|
||||||
name string
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Expression = Variable{}
|
func (v *Variable) Copy() Expression {
|
||||||
|
return NewVariable(v.Value)
|
||||||
func (v Variable) Name() string {
|
|
||||||
return v.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v Variable) String() string {
|
func (v *Variable) Accept(visitor Visitor) {
|
||||||
return v.name
|
visitor.VisitVariable(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVariable(name string) Variable {
|
func NewVariable(name string) *Variable {
|
||||||
return Variable{name}
|
return &Variable{Value: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Visitor interface {
|
||||||
|
VisitAbstraction(*Abstraction)
|
||||||
|
VisitApplication(*Application)
|
||||||
|
VisitVariable(*Variable)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import (
|
|||||||
"git.maximhutz.com/max/lambda/pkg/set"
|
"git.maximhutz.com/max/lambda/pkg/set"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateFreshName generates a variable name that is not in the used set.
|
func GenerateFreshName(used *set.Set[string]) string {
|
||||||
// This function does not mutate the used set.
|
|
||||||
func GenerateFreshName(used set.Set[string]) string {
|
|
||||||
for i := uint64(0); ; i++ {
|
for i := uint64(0); ; i++ {
|
||||||
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
|
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ package lambda
|
|||||||
|
|
||||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
|
||||||
func (e Variable) GetFree() set.Set[string] {
|
func GetFreeVariables(e Expression) *set.Set[string] {
|
||||||
return set.New(e.Name())
|
switch e := e.(type) {
|
||||||
}
|
case *Variable:
|
||||||
|
return set.New(e.Value)
|
||||||
func (e Abstraction) GetFree() set.Set[string] {
|
case *Abstraction:
|
||||||
vars := e.Body().GetFree()
|
vars := GetFreeVariables(e.Body)
|
||||||
vars.Remove(e.Parameter())
|
vars.Remove(e.Parameter)
|
||||||
return vars
|
return vars
|
||||||
}
|
case *Application:
|
||||||
|
vars := GetFreeVariables(e.Abstraction)
|
||||||
func (e Application) GetFree() set.Set[string] {
|
vars.Merge(GetFreeVariables(e.Argument))
|
||||||
vars := e.Abstraction().GetFree()
|
|
||||||
vars.Merge(e.Argument().GetFree())
|
|
||||||
return vars
|
return vars
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
func (e Variable) IsFree(n string) bool {
|
func IsFreeVariable(n string, e Expression) bool {
|
||||||
return e.Name() == n
|
switch e := e.(type) {
|
||||||
}
|
case *Variable:
|
||||||
|
return e.Value == n
|
||||||
func (e Abstraction) IsFree(n string) bool {
|
case *Abstraction:
|
||||||
return e.Parameter() != n && e.Body().IsFree(n)
|
return e.Parameter != n && IsFreeVariable(n, e.Body)
|
||||||
}
|
case *Application:
|
||||||
func (e Application) IsFree(n string) bool {
|
return IsFreeVariable(n, e.Abstraction) || IsFreeVariable(n, e.Argument)
|
||||||
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package lambda
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Marshaler struct{}
|
|
||||||
|
|
||||||
func (m Marshaler) Decode(string) (Expression, error) {
|
|
||||||
return nil, fmt.Errorf("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Marshaler) Encode(e Expression) (string, error) {
|
|
||||||
return e.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ codec.Marshaler[Expression] = (*Marshaler)(nil)
|
|
||||||
27
pkg/lambda/reduce.go
Normal file
27
pkg/lambda/reduce.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
import "git.maximhutz.com/max/lambda/pkg/fifo"
|
||||||
|
|
||||||
|
func ReduceOnce(e *Expression) bool {
|
||||||
|
stack := fifo.New(e)
|
||||||
|
|
||||||
|
for !stack.Empty() {
|
||||||
|
top := stack.MustPop()
|
||||||
|
|
||||||
|
switch typed := (*top).(type) {
|
||||||
|
case *Abstraction:
|
||||||
|
stack.Push(&typed.Body)
|
||||||
|
case *Application:
|
||||||
|
if fn, fnOk := typed.Abstraction.(*Abstraction); fnOk {
|
||||||
|
Substitute(&fn.Body, fn.Parameter, typed.Argument)
|
||||||
|
*top = fn.Body
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.Push(&typed.Argument)
|
||||||
|
stack.Push(&typed.Abstraction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,28 +1,19 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
// Rename replaces all occurrences of the target variable name with the new name.
|
func Rename(e Expression, target string, substitute string) {
|
||||||
func (e Variable) Rename(target string, newName string) Expression {
|
switch e := e.(type) {
|
||||||
if e.Name() == target {
|
case *Variable:
|
||||||
return NewVariable(newName)
|
if e.Value == target {
|
||||||
|
e.Value = substitute
|
||||||
|
}
|
||||||
|
case *Abstraction:
|
||||||
|
if e.Parameter == target {
|
||||||
|
e.Parameter = substitute
|
||||||
}
|
}
|
||||||
|
|
||||||
return e
|
Rename(e.Body, target, substitute)
|
||||||
}
|
case *Application:
|
||||||
|
Rename(e.Abstraction, target, substitute)
|
||||||
func (e Abstraction) Rename(target string, newName string) Expression {
|
Rename(e.Argument, target, substitute)
|
||||||
newParam := e.Parameter()
|
|
||||||
if e.Parameter() == target {
|
|
||||||
newParam = newName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newBody := e.Body().Rename(target, newName)
|
|
||||||
|
|
||||||
return NewAbstraction(newParam, newBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Application) Rename(target string, newName string) Expression {
|
|
||||||
newAbs := e.Abstraction().Rename(target, newName)
|
|
||||||
newArg := e.Argument().Rename(target, newName)
|
|
||||||
|
|
||||||
return NewApplication(newAbs, newArg)
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
pkg/lambda/stringify.go
Normal file
32
pkg/lambda/stringify.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type stringifyVisitor struct {
|
||||||
|
builder strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
||||||
|
v.builder.WriteString(a.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
|
||||||
|
v.builder.WriteRune('\\')
|
||||||
|
v.builder.WriteString(f.Parameter)
|
||||||
|
v.builder.WriteRune('.')
|
||||||
|
f.Body.Accept(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *stringifyVisitor) VisitApplication(c *Application) {
|
||||||
|
v.builder.WriteRune('(')
|
||||||
|
c.Abstraction.Accept(v)
|
||||||
|
v.builder.WriteRune(' ')
|
||||||
|
c.Argument.Accept(v)
|
||||||
|
v.builder.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stringify(e Expression) string {
|
||||||
|
b := &stringifyVisitor{builder: strings.Builder{}}
|
||||||
|
e.Accept(b)
|
||||||
|
return b.builder.String()
|
||||||
|
}
|
||||||
@@ -1,35 +1,27 @@
|
|||||||
package lambda
|
package lambda
|
||||||
|
|
||||||
func (e Variable) Substitute(target string, replacement Expression) Expression {
|
func Substitute(e *Expression, target string, replacement Expression) {
|
||||||
if e.Name() == target {
|
switch typed := (*e).(type) {
|
||||||
return replacement
|
case *Variable:
|
||||||
|
if typed.Value == target {
|
||||||
|
*e = replacement.Copy()
|
||||||
|
}
|
||||||
|
case *Abstraction:
|
||||||
|
if typed.Parameter == target {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return e
|
if IsFreeVariable(typed.Parameter, replacement) {
|
||||||
}
|
replacementFreeVars := GetFreeVariables(replacement)
|
||||||
|
used := GetFreeVariables(typed.Body)
|
||||||
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
|
used.Merge(replacementFreeVars)
|
||||||
if e.Parameter() == target {
|
freshVar := GenerateFreshName(used)
|
||||||
return e
|
Rename(typed, typed.Parameter, freshVar)
|
||||||
}
|
}
|
||||||
|
|
||||||
body := e.Body()
|
Substitute(&typed.Body, target, replacement)
|
||||||
param := e.Parameter()
|
case *Application:
|
||||||
if replacement.IsFree(param) {
|
Substitute(&typed.Abstraction, target, replacement)
|
||||||
freeVars := replacement.GetFree()
|
Substitute(&typed.Argument, target, replacement)
|
||||||
freeVars.Merge(body.GetFree())
|
|
||||||
freshVar := GenerateFreshName(freeVars)
|
|
||||||
body = body.Rename(param, freshVar)
|
|
||||||
param = freshVar
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newBody := body.Substitute(target, replacement)
|
|
||||||
return NewAbstraction(param, newBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Application) Substitute(target string, replacement Expression) Expression {
|
|
||||||
abs := e.Abstraction().Substitute(target, replacement)
|
|
||||||
arg := e.Argument().Substitute(target, replacement)
|
|
||||||
|
|
||||||
return NewApplication(abs, arg)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ type Expression interface {
|
|||||||
IsExpression()
|
IsExpression()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
type Abstraction struct {
|
type Abstraction struct {
|
||||||
Parameters []string
|
Parameters []string
|
||||||
Body Expression
|
Body Expression
|
||||||
@@ -30,8 +28,6 @@ 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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
// Package "saccharine" provides a simple language built on top of λ-calculus,
|
|
||||||
// to facilitate productive coding using it.
|
|
||||||
package saccharine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Marshaler struct{}
|
|
||||||
|
|
||||||
func (m Marshaler) Decode(s string) (Expression, error) {
|
|
||||||
tokens, err := scan(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return parse(tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Marshaler) Encode(e Expression) (string, error) {
|
|
||||||
return stringifyExpression(e), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ codec.Marshaler[Expression] = (*Marshaler)(nil)
|
|
||||||
@@ -5,17 +5,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/pkg/iterator"
|
"git.maximhutz.com/max/lambda/pkg/iterator"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
|
||||||
"git.maximhutz.com/max/lambda/pkg/trace"
|
"git.maximhutz.com/max/lambda/pkg/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenIterator = iterator.Iterator[Token]
|
type TokenIterator = iterator.Iterator[token.Token]
|
||||||
|
|
||||||
func parseRawToken(i *TokenIterator, expected TokenType) (*Token, error) {
|
func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) {
|
||||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
|
||||||
if tok, err := i.Next(); err != nil {
|
if tok, err := i.Next(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if tok.Type != expected {
|
} else if tok.Type != expected {
|
||||||
return nil, fmt.Errorf("expected token %v, got %v'", expected.Name(), tok.Value)
|
return nil, fmt.Errorf("expected token %v, got %v'", token.Name(expected), tok.Value)
|
||||||
} else {
|
} else {
|
||||||
return &tok, nil
|
return &tok, nil
|
||||||
}
|
}
|
||||||
@@ -24,14 +25,14 @@ func parseRawToken(i *TokenIterator, expected TokenType) (*Token, error) {
|
|||||||
|
|
||||||
func passSoftBreaks(i *TokenIterator) {
|
func passSoftBreaks(i *TokenIterator) {
|
||||||
for {
|
for {
|
||||||
if _, err := parseRawToken(i, TokenSoftBreak); err != nil {
|
if _, err := parseRawToken(i, token.SoftBreak); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseToken(i *TokenIterator, expected TokenType, ignoreSoftBreaks bool) (*Token, error) {
|
func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*token.Token, error) {
|
||||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
|
||||||
if ignoreSoftBreaks {
|
if ignoreSoftBreaks {
|
||||||
passSoftBreaks(i)
|
passSoftBreaks(i)
|
||||||
}
|
}
|
||||||
@@ -41,17 +42,17 @@ func parseToken(i *TokenIterator, expected TokenType, ignoreSoftBreaks bool) (*T
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseString(i *TokenIterator) (string, error) {
|
func parseString(i *TokenIterator) (string, error) {
|
||||||
if tok, err := parseToken(i, TokenAtom, true); err != nil {
|
if tok, err := parseToken(i, token.Atom, true); err != nil {
|
||||||
return "", trace.Wrap(err, "no variable (col %d)", i.Index())
|
return "", trace.Wrap(err, "no variable (col %d)", i.Index())
|
||||||
} else {
|
} else {
|
||||||
return tok.Value, nil
|
return tok.Value, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseBreak(i *TokenIterator) (*Token, error) {
|
func parseBreak(i *TokenIterator) (*token.Token, error) {
|
||||||
if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == nil {
|
if tok, softErr := parseRawToken(i, token.SoftBreak); softErr == nil {
|
||||||
return tok, nil
|
return tok, nil
|
||||||
} else if tok, hardErr := parseRawToken(i, TokenHardBreak); hardErr == nil {
|
} else if tok, hardErr := parseRawToken(i, token.HardBreak); hardErr == nil {
|
||||||
return tok, nil
|
return tok, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.Join(softErr, hardErr)
|
return nil, errors.Join(softErr, hardErr)
|
||||||
@@ -75,11 +76,11 @@ func parseList[U any](i *TokenIterator, fn func(*TokenIterator) (U, error), mini
|
|||||||
|
|
||||||
func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
||||||
return iterator.Do(i, func(i *TokenIterator) (*Abstraction, error) {
|
return iterator.Do(i, func(i *TokenIterator) (*Abstraction, error) {
|
||||||
if _, err := parseToken(i, TokenSlash, true); err != nil {
|
if _, err := parseToken(i, token.Slash, true); err != nil {
|
||||||
return nil, trace.Wrap(err, "no function slash (col %d)", i.MustGet().Column)
|
return nil, trace.Wrap(err, "no function slash (col %d)", i.MustGet().Column)
|
||||||
} else if parameters, err := parseList(i, parseString, 0); err != nil {
|
} else if parameters, err := parseList(i, parseString, 0); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if _, err = parseToken(i, TokenDot, true); err != nil {
|
} else if _, err = parseToken(i, token.Dot, true); err != nil {
|
||||||
return nil, trace.Wrap(err, "no function dot (col %d)", i.MustGet().Column)
|
return nil, trace.Wrap(err, "no function dot (col %d)", i.MustGet().Column)
|
||||||
} else if body, err := parseExpression(i); err != nil {
|
} else if body, err := parseExpression(i); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -91,11 +92,11 @@ func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
|||||||
|
|
||||||
func parseApplication(i *TokenIterator) (*Application, error) {
|
func parseApplication(i *TokenIterator) (*Application, error) {
|
||||||
return iterator.Do(i, func(i *TokenIterator) (*Application, error) {
|
return iterator.Do(i, func(i *TokenIterator) (*Application, error) {
|
||||||
if _, err := parseToken(i, TokenOpenParen, true); err != nil {
|
if _, err := parseToken(i, token.OpenParen, true); err != nil {
|
||||||
return nil, trace.Wrap(err, "no openning brackets (col %d)", i.MustGet().Column)
|
return nil, trace.Wrap(err, "no openning brackets (col %d)", i.MustGet().Column)
|
||||||
} else if expressions, err := parseList(i, parseExpression, 1); err != nil {
|
} else if expressions, err := parseList(i, parseExpression, 1); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if _, err := parseToken(i, TokenCloseParen, true); err != nil {
|
} else if _, err := parseToken(i, token.CloseParen, true); err != nil {
|
||||||
return nil, trace.Wrap(err, "no closing brackets (col %d)", i.MustGet().Column)
|
return nil, trace.Wrap(err, "no closing brackets (col %d)", i.MustGet().Column)
|
||||||
} else {
|
} else {
|
||||||
return NewApplication(expressions[0], expressions[1:]), nil
|
return NewApplication(expressions[0], expressions[1:]), nil
|
||||||
@@ -104,7 +105,7 @@ func parseApplication(i *TokenIterator) (*Application, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseAtom(i *TokenIterator) (*Atom, error) {
|
func parseAtom(i *TokenIterator) (*Atom, error) {
|
||||||
if tok, err := parseToken(i, TokenAtom, true); err != nil {
|
if tok, err := parseToken(i, token.Atom, true); err != nil {
|
||||||
return nil, trace.Wrap(err, "no variable (col %d)", i.Index())
|
return nil, trace.Wrap(err, "no variable (col %d)", i.Index())
|
||||||
} else {
|
} else {
|
||||||
return NewAtom(tok.Value), nil
|
return NewAtom(tok.Value), nil
|
||||||
@@ -132,7 +133,7 @@ func parseStatements(i *TokenIterator) ([]Statement, error) {
|
|||||||
|
|
||||||
func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
||||||
if braces {
|
if braces {
|
||||||
if _, err := parseToken(i, TokenOpenBrace, true); err != nil {
|
if _, err := parseToken(i, token.OpenBrace, true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +152,7 @@ func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if braces {
|
if braces {
|
||||||
if _, err := parseToken(i, TokenCloseBrace, true); err != nil {
|
if _, err := parseToken(i, token.CloseBrace, true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,13 +165,13 @@ func parseExpression(i *TokenIterator) (Expression, error) {
|
|||||||
passSoftBreaks(i)
|
passSoftBreaks(i)
|
||||||
|
|
||||||
switch peek := i.MustGet(); peek.Type {
|
switch peek := i.MustGet(); peek.Type {
|
||||||
case TokenOpenParen:
|
case token.OpenParen:
|
||||||
return parseApplication(i)
|
return parseApplication(i)
|
||||||
case TokenSlash:
|
case token.Slash:
|
||||||
return parseAbstraction(i)
|
return parseAbstraction(i)
|
||||||
case TokenAtom:
|
case token.Atom:
|
||||||
return parseAtom(i)
|
return parseAtom(i)
|
||||||
case TokenOpenBrace:
|
case token.OpenBrace:
|
||||||
return parseClause(i, true)
|
return parseClause(i, true)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column)
|
return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column)
|
||||||
@@ -182,7 +183,7 @@ func parseLet(i *TokenIterator) (*LetStatement, error) {
|
|||||||
return iterator.Do(i, func(i *TokenIterator) (*LetStatement, error) {
|
return iterator.Do(i, func(i *TokenIterator) (*LetStatement, error) {
|
||||||
if parameters, err := parseList(i, parseString, 1); err != nil {
|
if parameters, err := parseList(i, parseString, 1); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if _, err := parseToken(i, TokenAssign, true); err != nil {
|
} else if _, err := parseToken(i, token.Assign, true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if body, err := parseExpression(i); err != nil {
|
} else if body, err := parseExpression(i); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -211,7 +212,7 @@ func parseStatement(i *TokenIterator) (Statement, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Given a list of tokens, attempt to parse it into an syntax tree.
|
// Given a list of tokens, attempt to parse it into an syntax tree.
|
||||||
func parse(tokens []Token) (Expression, error) {
|
func parse(tokens []token.Token) (Expression, error) {
|
||||||
i := iterator.Of(tokens)
|
i := iterator.Of(tokens)
|
||||||
|
|
||||||
exp, err := parseClause(i, false)
|
exp, err := parseClause(i, false)
|
||||||
|
|||||||
22
pkg/saccharine/saccharine.go
Normal file
22
pkg/saccharine/saccharine.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Package "saccharine" provides a simple language built on top of λ-calculus,
|
||||||
|
// to facilitate productive coding using it.
|
||||||
|
package saccharine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Convert a piece of valid saccharine code into an expression.
|
||||||
|
func Parse(code string) (Expression, error) {
|
||||||
|
tokens, err := token.Parse(code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a parsed saccharine expression back into source code.
|
||||||
|
func Stringify(expression Expression) string {
|
||||||
|
return stringifyExpression(expression)
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@ type Statement interface {
|
|||||||
IsStatement()
|
IsStatement()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
type LetStatement struct {
|
type LetStatement struct {
|
||||||
Name string
|
Name string
|
||||||
Parameters []string
|
Parameters []string
|
||||||
@@ -19,8 +17,6 @@ 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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
package saccharine
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// All tokens in the pseudo-lambda language.
|
|
||||||
type TokenType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TokenOpenParen TokenType = iota // Denotes the '(' token.
|
|
||||||
TokenCloseParen // Denotes the ')' token.
|
|
||||||
TokenOpenBrace // Denotes the '{' token.
|
|
||||||
TokenCloseBrace // Denotes the '}' token.
|
|
||||||
TokenHardBreak // Denotes the ';' token.
|
|
||||||
TokenAssign // Denotes the ':=' token.
|
|
||||||
TokenAtom // Denotes an alpha-numeric variable.
|
|
||||||
TokenSlash // Denotes the '/' token.
|
|
||||||
TokenDot // Denotes the '.' token.
|
|
||||||
TokenSoftBreak // Denotes a new-line.
|
|
||||||
)
|
|
||||||
|
|
||||||
// A representation of a token in source code.
|
|
||||||
type Token struct {
|
|
||||||
Column int // Where the token begins in the source text.
|
|
||||||
Type TokenType // What type the token is.
|
|
||||||
Value string // The value of the token.
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenOpenParen(column int) *Token {
|
|
||||||
return &Token{Type: TokenOpenParen, Column: column, Value: "("}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenCloseParen(column int) *Token {
|
|
||||||
return &Token{Type: TokenCloseParen, Column: column, Value: ")"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenOpenBrace(column int) *Token {
|
|
||||||
return &Token{Type: TokenOpenBrace, Column: column, Value: "{"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenCloseBrace(column int) *Token {
|
|
||||||
return &Token{Type: TokenCloseBrace, Column: column, Value: "}"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenDot(column int) *Token {
|
|
||||||
return &Token{Type: TokenDot, Column: column, Value: "."}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenHardBreak(column int) *Token {
|
|
||||||
return &Token{Type: TokenHardBreak, Column: column, Value: ";"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenAssign(column int) *Token {
|
|
||||||
return &Token{Type: TokenAssign, Column: column, Value: ":="}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenSlash(column int) *Token {
|
|
||||||
return &Token{Type: TokenSlash, Column: column, Value: "\\"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenAtom(name string, column int) *Token {
|
|
||||||
return &Token{Type: TokenAtom, Column: column, Value: name}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTokenSoftBreak(column int) *Token {
|
|
||||||
return &Token{Type: TokenSoftBreak, Column: column, Value: "\\n"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TokenType) Name() string {
|
|
||||||
switch t {
|
|
||||||
case TokenOpenParen:
|
|
||||||
return "("
|
|
||||||
case TokenCloseParen:
|
|
||||||
return ")"
|
|
||||||
case TokenSlash:
|
|
||||||
return "\\"
|
|
||||||
case TokenDot:
|
|
||||||
return "."
|
|
||||||
case TokenAtom:
|
|
||||||
return "ATOM"
|
|
||||||
case TokenSoftBreak:
|
|
||||||
return "\\n"
|
|
||||||
case TokenHardBreak:
|
|
||||||
return ";"
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unknown token type %v", t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Token) Name() string {
|
|
||||||
return t.Type.Name()
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package saccharine
|
package token
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -14,7 +14,7 @@ func isVariable(r rune) bool {
|
|||||||
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
|
func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
|
||||||
i2 := i.Copy()
|
i2 := i.Copy()
|
||||||
|
|
||||||
if r, err := i2.Next(); err != nil {
|
if r, err := i2.Next(); err != nil {
|
||||||
@@ -27,7 +27,7 @@ func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||||
i2 := i.Copy()
|
i2 := i.Copy()
|
||||||
|
|
||||||
if r, err := i2.Next(); err != nil {
|
if r, err := i2.Next(); err != nil {
|
||||||
@@ -42,7 +42,7 @@ func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
|||||||
|
|
||||||
// Pulls the next token from an iterator over runes. If it cannot, it will
|
// Pulls the next token from an iterator over runes. If it cannot, it will
|
||||||
// return nil. If an error occurs, it will return that.
|
// return nil. If an error occurs, it will return that.
|
||||||
func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
func getToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||||
index := i.Index()
|
index := i.Index()
|
||||||
|
|
||||||
if i.Done() {
|
if i.Done() {
|
||||||
@@ -56,69 +56,54 @@ func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case letter == '(':
|
case letter == '(':
|
||||||
return NewTokenOpenParen(index), nil
|
return NewOpenParen(index), nil
|
||||||
case letter == ')':
|
case letter == ')':
|
||||||
return NewTokenCloseParen(index), nil
|
return NewCloseParen(index), nil
|
||||||
case letter == '.':
|
case letter == '.':
|
||||||
return NewTokenDot(index), nil
|
return NewDot(index), nil
|
||||||
case letter == '\\':
|
case letter == '\\':
|
||||||
return NewTokenSlash(index), nil
|
return NewSlash(index), nil
|
||||||
case letter == '\n':
|
case letter == '\n':
|
||||||
return NewTokenSoftBreak(index), nil
|
return NewSoftBreak(index), nil
|
||||||
case letter == '{':
|
case letter == '{':
|
||||||
return NewTokenOpenBrace(index), nil
|
return NewOpenBrace(index), nil
|
||||||
case letter == '}':
|
case letter == '}':
|
||||||
return NewTokenCloseBrace(index), nil
|
return NewCloseBrace(index), nil
|
||||||
case letter == ':':
|
case letter == ':':
|
||||||
if _, err := scanCharacter(i, '='); err != nil {
|
if _, err := parseCharacter(i, '='); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return NewTokenAssign(index), nil
|
return NewAssign(index), nil
|
||||||
}
|
}
|
||||||
case letter == ';':
|
case letter == ';':
|
||||||
return NewTokenHardBreak(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):
|
||||||
atom := []rune{letter}
|
atom := []rune{letter}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if r, err := scanRune(i, isVariable); err != nil {
|
if r, err := parseRune(i, isVariable); err != nil {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
atom = append(atom, r)
|
atom = append(atom, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewTokenAtom(string(atom), index), nil
|
return NewAtom(string(atom), index), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unknown character '%v'", string(letter))
|
return nil, fmt.Errorf("unknown character '%v'", string(letter))
|
||||||
}
|
}
|
||||||
|
|
||||||
// scan a string into tokens.
|
// Parse a string into tokens.
|
||||||
func scan(input string) ([]Token, error) {
|
func Parse(input string) ([]Token, error) {
|
||||||
i := iterator.Of([]rune(input))
|
i := iterator.Of([]rune(input))
|
||||||
tokens := []Token{}
|
tokens := []Token{}
|
||||||
errorList := []error{}
|
errorList := []error{}
|
||||||
|
|
||||||
for !i.Done() {
|
for !i.Done() {
|
||||||
token, err := scanToken(i)
|
token, err := getToken(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorList = append(errorList, err)
|
errorList = append(errorList, err)
|
||||||
} else if token != nil {
|
} else if token != nil {
|
||||||
91
pkg/saccharine/token/token.go
Normal file
91
pkg/saccharine/token/token.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// All tokens in the pseudo-lambda language.
|
||||||
|
type Type int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpenParen Type = iota // Denotes the '(' token.
|
||||||
|
CloseParen // Denotes the ')' token.
|
||||||
|
OpenBrace // Denotes the '{' token.
|
||||||
|
CloseBrace // Denotes the '}' token.
|
||||||
|
HardBreak // Denotes the ';' token.
|
||||||
|
Assign // Denotes the ':=' token.
|
||||||
|
Atom // Denotes an alpha-numeric variable.
|
||||||
|
Slash // Denotes the '/' token.
|
||||||
|
Dot // Denotes the '.' token.
|
||||||
|
SoftBreak // Denotes a new-line.
|
||||||
|
)
|
||||||
|
|
||||||
|
// A representation of a token in source code.
|
||||||
|
type Token struct {
|
||||||
|
Column int // Where the token begins in the source text.
|
||||||
|
Type Type // What type the token is.
|
||||||
|
Value string // The value of the token.
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenParen(column int) *Token {
|
||||||
|
return &Token{Type: OpenParen, Column: column, Value: "("}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloseParen(column int) *Token {
|
||||||
|
return &Token{Type: CloseParen, Column: column, Value: ")"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenBrace(column int) *Token {
|
||||||
|
return &Token{Type: OpenBrace, Column: column, Value: "{"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloseBrace(column int) *Token {
|
||||||
|
return &Token{Type: CloseBrace, Column: column, Value: "}"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDot(column int) *Token {
|
||||||
|
return &Token{Type: Dot, Column: column, Value: "."}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHardBreak(column int) *Token {
|
||||||
|
return &Token{Type: HardBreak, Column: column, Value: ";"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAssign(column int) *Token {
|
||||||
|
return &Token{Type: Assign, Column: column, Value: ":="}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSlash(column int) *Token {
|
||||||
|
return &Token{Type: Slash, Column: column, Value: "\\"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAtom(name string, column int) *Token {
|
||||||
|
return &Token{Type: Atom, Column: column, Value: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSoftBreak(column int) *Token {
|
||||||
|
return &Token{Type: SoftBreak, Column: column, Value: "\\n"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Name(typ Type) string {
|
||||||
|
switch typ {
|
||||||
|
case OpenParen:
|
||||||
|
return "("
|
||||||
|
case CloseParen:
|
||||||
|
return ")"
|
||||||
|
case Slash:
|
||||||
|
return "\\"
|
||||||
|
case Dot:
|
||||||
|
return "."
|
||||||
|
case Atom:
|
||||||
|
return "ATOM"
|
||||||
|
case SoftBreak:
|
||||||
|
return "\\n"
|
||||||
|
case HardBreak:
|
||||||
|
return ";"
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown token type %v", typ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Token) Name() string {
|
||||||
|
return Name(t.Type)
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package set
|
package set
|
||||||
|
|
||||||
import "iter"
|
|
||||||
|
|
||||||
type Set[T comparable] map[T]bool
|
type Set[T comparable] map[T]bool
|
||||||
|
|
||||||
func (s Set[T]) Add(items ...T) {
|
func (s *Set[T]) Add(items ...T) {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
s[item] = true
|
(*s)[item] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,14 +12,14 @@ func (s Set[T]) Has(item T) bool {
|
|||||||
return s[item]
|
return s[item]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Set[T]) Remove(items ...T) {
|
func (s *Set[T]) Remove(items ...T) {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
delete(s, item)
|
delete(*s, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Set[T]) Merge(o Set[T]) {
|
func (s *Set[T]) Merge(o *Set[T]) {
|
||||||
for item := range o {
|
for item := range *o {
|
||||||
s.Add(item)
|
s.Add(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,18 +34,8 @@ func (s Set[T]) ToList() []T {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Set[T]) Items() iter.Seq[T] {
|
func New[T comparable](items ...T) *Set[T] {
|
||||||
return func(yield func(T) bool) {
|
result := &Set[T]{}
|
||||||
for item := range s {
|
|
||||||
if !yield(item) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func New[T comparable](items ...T) Set[T] {
|
|
||||||
result := Set[T]{}
|
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
result.Add(item)
|
result.Add(item)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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)))))
|
||||||
|
|
||||||
(print (exp N N))
|
(exp N N)
|
||||||
16
samples/simple.txt
Normal file
16
samples/simple.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
(\0.
|
||||||
|
(\inc.
|
||||||
|
(\add.
|
||||||
|
(\mult.
|
||||||
|
(\exp.
|
||||||
|
(exp (inc (inc (inc (inc 0)))) (inc (inc (inc (inc (inc 0))))))
|
||||||
|
\n m.(m n)
|
||||||
|
)
|
||||||
|
\m n f.(m (n f))
|
||||||
|
)
|
||||||
|
\n m.(m inc n)
|
||||||
|
)
|
||||||
|
\n f x.(f (n f x))
|
||||||
|
)
|
||||||
|
\f x.x
|
||||||
|
)
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
VALUE
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# This is a full-line comment at the start
|
|
||||||
# The following defines the identity function
|
|
||||||
identity := \x.x # This is an end-of-line comment
|
|
||||||
|
|
||||||
# Define a simple function that applies a function twice
|
|
||||||
twice := \f.\x.(f
|
|
||||||
# Comments can be anywhere!
|
|
||||||
(f x))
|
|
||||||
|
|
||||||
# Test that comments don't interfere with expressions
|
|
||||||
result := (twice identity VALUE) # Should just return VALUE
|
|
||||||
|
|
||||||
# Multiple comments in a row
|
|
||||||
# can appear anywhere
|
|
||||||
# without breaking the code
|
|
||||||
|
|
||||||
result # Final comment at the end
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
(0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (1 END)))))))))))))))))))))))))))))))
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
(0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (0 (1 END)))))))))))))))))))))))))))))))
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
VALUE
|
|
||||||
Reference in New Issue
Block a user