18 Commits

Author SHA1 Message Date
31924237b2 feat: saccharine marshaler 2026-01-30 20:16:57 -05:00
68cc1624c7 feat: registry, normalorder engine, lambda codec and marshaler 2026-01-30 20:07:35 -05:00
0cdce0e42c feat: cli versions 2026-01-30 17:54:47 -05:00
0ec52008bb feat: repr, codec 2026-01-30 16:24:17 -05:00
f2c8d9f7d2 fix: use loop variable instead of global ticker in GenerateFreshName (#40)
## Description

`GenerateFreshName` used a global `ticker` variable but never incremented it inside the loop.
This caused an infinite loop if the first generated name (`_0`) was already in the used set.

- Remove global `ticker` variable.
- Use loop variable `i` directly to generate candidate names.

## Benefits

- Fixes infinite loop bug when generated name collides with used set.
- Removes unnecessary global state.
- Simpler and more predictable behavior.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Reviewed-on: #40
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-18 20:58:23 +00:00
9c7fb8ceba refactor: rename interpreter to runtime and use receiver methods (#39)
## Description

The codebase previously used "interpreter" terminology and standalone functions for expression operations.
This PR modernizes the architecture by renaming to "runtime" and converting operations to receiver methods.

- Rename `pkg/interpreter` to `pkg/runtime`.
- Move `ReduceOnce` to new `pkg/normalorder` package for reduction strategy isolation.
- Convert standalone functions (`Substitute`, `Rename`, `GetFree`, `IsFree`) to receiver methods on concrete expression types.
- Change `Set` from pointer receivers to value receivers for simpler usage.
- Update all references from "interpreter" to "runtime" terminology throughout the codebase.

### Decisions

- Operations like `Substitute`, `Rename`, `GetFree`, and `IsFree` are now methods on the `Expression` interface, implemented by each concrete type (`Variable`, `Abstraction`, `Application`).
- The `normalorder` package isolates the normal-order reduction strategy, allowing future reduction strategies to be added in separate packages.
- `Set` uses value receivers since Go maps are reference types and don't require pointer semantics.

## Benefits

- Cleaner API: `expr.Substitute(target, replacement)` instead of `Substitute(expr, target, replacement)`.
- Better separation of concerns: reduction strategies are isolated from expression types.
- Consistent terminology: "runtime" better reflects the execution model.
- Simpler `Set` usage without needing to manage pointers.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Reviewed-on: #39
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-18 20:52:34 +00:00
e85cf7ceff refactor: make lambda expression types immutable (#38)
## Summary

- Change Abstraction, Application, and Variable to use private fields with getter methods.
- Return value types instead of pointers from constructors.
- Update all type switches to match value types instead of pointer types.

## Test plan

- [x] All existing tests pass (`make test`).

Reviewed-on: #38
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-17 22:00:54 +00:00
c2aa77cb92 refactor: remove visitor pattern (#37)
## Description

The codebase previously used the visitor pattern for traversing lambda calculus expressions.
This was a hold-over from avoiding the Go-idiomatic way of handling types.
This PR removes the visitor pattern in favor of direct method implementations.

- Remove `Visitor` interface from `expression.go`.
- Remove `Accept` methods from `Abstraction`, `Application`, and `Variable`.
- Remove `Accept` from `Expression` interface.
- Delete `stringify.go` and move `String()` logic directly into each type.
- Add compile-time interface checks (`var _ Expression = (*Type)(nil)`).
- Update `expr.Expression` to embed `fmt.Stringer` instead of declaring `String() string`.

### Decisions

- Moved `String()` implementations directly into each expression type rather than using a separate recursive function, as each type's string representation is simple enough to be self-contained.

## Benefits

- Simpler, more idiomatic Go code using type methods instead of visitor pattern.
- Reduced indirection and fewer files to maintain.
- Compile-time interface satisfaction checks catch implementation errors early.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`).
- [x] Tests pass (if applicable).
- [ ] Documentation updated (if applicable).

Closes #36

Reviewed-on: #37
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-17 20:46:07 +00:00
52d40adcc6 chore: remove unused deltanet package (#35)
## Description

The `deltanet` package was an unused stub in the codebase.
This PR removes it to reduce clutter.

- Removed `pkg/deltanet/deltanet.go`.
- Removed `pkg/deltanet/node.go`.

Closes #34

## Benefits

- Reduces codebase complexity by removing unused code.
- Eliminates potential confusion from an incomplete stub package.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`).
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Reviewed-on: #35
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-17 19:56:58 +00:00
1974ad582f refactor: move event system to reducer, remove engine package (#32)
## Description

This PR completes the MVC-inspired refactoring by moving the event system from the engine into the reducer.
The engine package is now removed entirely, as the reducer handles both reduction logic and lifecycle events.

- Add `pkg/reducer/events.go` with `StartEvent`, `StepEvent`, and `StopEvent`.
- Extend `Reducer` interface to embed `Emitter[Event]` and add `Expression()` method.
- Update `NormalOrderReducer` to embed `BaseEmitter` and emit lifecycle events during reduction.
- Update all plugins to attach to `Reducer` instead of `Engine`.
- Remove `internal/engine` package entirely.
- Add `Off()` method to `BaseEmitter` to complete the `Emitter` interface.
- Fix `Emitter.On` signature to use generic type `E` instead of `string`.

### Decisions

- The `Reducer` interface now combines reduction logic with event emission, making it the single orchestration point.
- Plugins attach directly to the reducer, simplifying the architecture.
- The `Expression()` method on `Reducer` provides access to current state for plugins.

## Benefits

- Simpler architecture with one fewer abstraction layer.
- Plugins are now mode-agnostic - they work with any `Reducer` implementation.
- Cleaner separation: reducers handle reduction, plugins observe via events.
- Easier to add new evaluation modes - just implement `Reducer` with embedded emitter.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`).
- [x] Tests pass (if applicable).
- [ ] Documentation updated (if applicable).

Reviewed-on: #32
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-17 00:27:36 +00:00
f8e1223463 refactor: extract Reducer interface and update engine to use abstractions (#31)
## Description

This PR builds on #30 to complete the abstraction layer for multi-mode evaluation support.
The engine now accepts abstract `expr.Expression` and `reducer.Reducer` types instead of concrete lambda types.

- Add `pkg/reducer/reducer.go` with `Reducer` interface defining `Reduce(expr.Expression, onStep) expr.Expression`.
- Add `pkg/lambda/reducer.go` with `NormalOrderReducer` that wraps the existing `ReduceAll` logic.
- Update `engine.Engine` to store `expr.Expression` and `reducer.Reducer` instead of `*lambda.Expression`.
- Update plugins to use `expr.Expression.String()` directly (no pointer dereference needed).
- Update main and tests to instantiate `NormalOrderReducer` and pass it to the engine.

### Decisions

- The `Reducer.Reduce` method returns the final expression and calls `onStep` after each reduction step with the current state.
- `NormalOrderReducer` type-asserts to `lambda.Expression` internally; other expression types are returned unchanged.
- The engine updates its `Expression` field both during reduction (via `onStep`) and after completion.

## Benefits

- The engine is now fully decoupled from lambda-specific types.
- New evaluation modes can be added by implementing `expr.Expression` and `reducer.Reducer`.
- Plugins work with any expression type that implements `expr.Expression`.
- Prepares the codebase for SKI combinators, typed lambda calculus, or other future modes.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`).
- [x] Tests pass (if applicable).
- [ ] Documentation updated (if applicable).

Closes #30

Reviewed-on: #31
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-16 23:42:07 +00:00
e0114c736d refactor: extract abstract Expression interface (#30)
## Description

The codebase currently couples the engine and plugins directly to `lambda.Expression`.
This PR introduces an abstract `expr.Expression` interface to enable future support for multiple evaluation modes.

- Add `pkg/expr/expr.go` with an `Expression` interface requiring a `String()` method.
- Update `lambda.Expression` to embed `expr.Expression`.
- Add `String()` method to `Abstraction`, `Application`, and `Variable` types.
- Update plugins to use `String()` instead of `lambda.Stringify()`.

### Decisions

- The `expr.Expression` interface is minimal (only `String()`) to avoid over-constraining future expression types.
- The engine still stores `*lambda.Expression` directly rather than `expr.Expression`, because Go's interface semantics require pointer indirection for in-place mutation during reduction.
- Future evaluation modes will implement their own concrete types satisfying `expr.Expression`.

## Benefits

- Establishes a foundation for supporting multiple evaluation modes (SKI combinators, typed lambda calculus, etc.).
- Plugins now use the abstract `String()` method, making them more decoupled from the lambda-specific implementation.
- Prepares the codebase for a Reducer interface abstraction in a future PR.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`).
- [x] Tests pass (if applicable).
- [ ] Documentation updated (if applicable).

Reviewed-on: #30
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-16 23:37:31 +00:00
5c54f4e195 fix: correct event handler registration in plugins (#29)
## Description

This PR fixes incorrect event handler registration in two plugins that were introduced in the refactoring.
The bugs prevented the plugins from functioning as intended.

Fixed issues:
- Statistics plugin was registering `plugin.Step` for `StopEvent` instead of `plugin.Stop`, preventing statistics from being printed at the end of execution.
- Logs plugin was listening to `StopEvent` instead of `StepEvent`, causing it to log only once at the end instead of on each reduction step.

## Benefits

Statistics are now correctly printed at the end of execution.
Debug logs now correctly show each reduction step instead of just the final state.
Plugins now work as originally intended before the refactoring.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`).
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Reviewed-on: #29
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-14 00:35:02 +00:00
307b7ffd1e refactor: replace string-based emitter with type-safe generic event system (#28)
## Description

This PR refactors the event emitter system from a string-based message passing approach to a type-safe generic implementation using typed events.
The previous system relied on string message names which were error-prone and lacked compile-time safety.
This refactoring introduces a generic `BaseEmitter[E comparable]` that provides type safety while consolidating the various tracker packages into a unified plugins architecture.

Key changes:
- Replace `Emitter` with generic `BaseEmitter[E comparable]` for type-safe event handling.
- Add `Event` type enumeration with `StartEvent`, `StepEvent`, and `StopEvent` constants.
- Create `Listener[E]` interface with `BaseListener` implementation for better abstraction.
- Consolidate `explanation`, `performance`, and `statistics` packages into unified `internal/plugins` package.
- Simplify CLI initialization by using plugin constructors that handle their own event subscriptions.
- Add `Items()` iterator method to `Set` for idiomatic Go 1.23+ range loops over sets.

### Decisions

Use generics for type-safe event handling.
This provides compile-time guarantees that event types match their handlers while maintaining flexibility for future event types.

Consolidate trackers into plugins architecture.
Previously separate packages (`explanation`, `performance`, `statistics`) now live under `internal/plugins`, making the plugin pattern explicit and easier to extend.

Plugin constructors self-register with engine.
Each plugin's `New*` constructor now handles its own event subscriptions, reducing boilerplate in the main CLI.

## Benefits

Type safety prevents runtime errors from typos in event names.
The compiler now catches mismatched event types at compile time rather than failing silently at runtime.

Cleaner plugin architecture makes adding new features easier.
New plugins follow a consistent pattern and live in a single location.

Reduced boilerplate in main CLI.
Plugin initialization is now a single function call rather than manual event registration.

Better testability through interface-based design.
The `Listener[E]` interface allows for easier mocking and testing of event handlers.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`).
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Reviewed-on: #28
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-14 00:30:21 +00:00
335ce95c50 feat: add comment support to saccharine language (#25)
## Description

The saccharine language previously lacked comment support, preventing proper code documentation.
This PR implements '#' comment syntax similar to Python.
Comments can appear on their own line or at the end of a line, with all content after '#' ignored until the next newline or EOF.
The tokenizer now detects '#' and skips characters appropriately without creating tokens.

### Decisions

Comments are silently consumed during tokenization rather than being preserved as tokens, keeping the token stream clean for the parser.
The implementation preserves newlines after comments by using the iterator's Back() method, allowing them to be processed as soft breaks.

## Benefits

Developers can now document their saccharine code with inline and full-line comments.
The implementation is minimal and efficient, adding no overhead to the token stream.
Tests verify that comments work correctly in various positions without breaking code execution.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Closes #24

Reviewed-on: #25
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-13 02:00:00 +00:00
19652563a4 docs: add issue linking instructions to PR workflow (#23)
## Description

This PR updates CLAUDE.md to include documentation about linking issues in pull requests.
The documentation explains how to use the "Closes #<number>" syntax in commit messages and PR descriptions to automatically link and close issues when PRs are merged.

### Changes

- Added "Linking issues" section to Pull Request Management in [CLAUDE.md](CLAUDE.md:81-82).
- Documented the use of `Closes #<number>` syntax for automatic issue closure.

## Benefits

Developers will know to properly link issues in their PRs.
Issues will be automatically closed when their corresponding PRs are merged.
The repository will maintain better traceability between issues and PRs.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Reviewed-on: #23
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-13 01:45:53 +00:00
dec9af0244 feat: add issue templates for Gitea (#22)
## Description

This PR implements issue templates for the lambda repository to improve issue tracking and developer experience.
The templates follow Gitea best practices and provide clear guidance for users when creating issues.
Three distinct templates were created to handle different types of issues: feature requests, bug reports, and general issues.

### Changes

- Created [.gitea/ISSUE_TEMPLATE/feature.md](.gitea/ISSUE_TEMPLATE/feature.md) for feature requests and enhancements.
- Created [.gitea/ISSUE_TEMPLATE/bug.md](.gitea/ISSUE_TEMPLATE/bug.md) for bug reports.
- Created [.gitea/ISSUE_TEMPLATE/general.md](.gitea/ISSUE_TEMPLATE/general.md) for general issues.
- Updated [CLAUDE.md](CLAUDE.md) with issue management and workflow documentation.

### Decisions

All templates use Markdown format with YAML frontmatter, following the existing PR template style.
Each template includes pre-populated metadata (title prefix, labels, target branch).
Templates provide inline HTML comments to guide users without cluttering the final issue.
The templates are consistent with conventional commit format (feat:, fix:, etc.).

## Benefits

Users will have clear guidance when creating issues, reducing back-and-forth communication.
Issues will be more structured and contain necessary information for developers.
Pre-populated labels and titles ensure consistent issue categorization.
The templates align with the existing PR template style for a cohesive experience.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [x] Tests pass (if applicable).
- [x] Documentation updated (if applicable).

Reviewed-on: #22
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-13 01:43:36 +00:00
dbc3c5a8d4 Improve testing infrastructure with dynamic discovery and validation (#20)
## Summary

This PR enhances the testing infrastructure with dynamic test discovery, automated validation, and improved error handling.

## Changes

### Testing Infrastructure
- Added `TestSamplesValidity` integration test that validates all test files against their expected output.
- Implemented dynamic test discovery using `filepath.Glob` to automatically find all `.test` files.
- Renamed `benchmark_test.go` to `lambda_test.go` for better naming consistency.
- Consolidated helper functions into a single `runSample` function.
- Replaced all error handling with `assert` for consistent and clear test output.
- Required all `.test` files to have corresponding `.expected` files.

### Iterator Improvements
- Added `Swap` method to iterator for better reduction algorithm.
- Improved reduction algorithm with LIFO-based iterator implementation.

### Build System
- Added `make test` target to run tests without benchmarks.
- Updated Makefile help text to include the new test target.

### Test Cases
- Added new test cases with expected outputs: `church_5^5`, `church_6^6`, `fast_list_2^30`, `list_2^30`.
- Added validation files for all test cases.

## Test plan

- Run tests with expected output validation.
- Run benchmarks to ensure performance is maintained.
- Verify make targets work correctly.

Reviewed-on: #20
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-01-13 01:20:47 +00:00
60 changed files with 1146 additions and 887 deletions

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ linters:
# More information: https://golangci-lint.run/usage/false-positives/#comments # More information: https://golangci-lint.run/usage/false-positives/#comments
# #
# Please uncomment the following line if your code is not using the godoc format # Please uncomment the following line if your code is not using the godoc format
- comments # - comments
# Common false positives # Common false positives
# feel free to remove this if you don't have any false positives # feel free to remove this if you don't have any false positives
@@ -126,6 +126,9 @@ linters:
# Blank import should be only in a main or test package, or have a comment justifying it. # 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. # context.Context() should be the first parameter of a function when provided as argument.
- name: context-as-argument - name: context-as-argument
arguments: arguments:

View File

@@ -37,6 +37,31 @@ Use format: `<type>/<description>` with kebab-case.
DO NOT advertise Claude. DO NOT advertise Claude.
## Issue Management
Use the `tea` CLI (Gitea command-line tool) for issue operations.
**Common commands**:
- `tea issues list` - List all issues.
- `tea issues <number>` - View details of a specific issue.
- `tea issues create --title "<title>" --body "<description>"` - Create a new issue.
- `tea issues close <number>` - Close an issue.
**Reading issues**: Use `tea issues <number>` to read the full details of an issue before starting work.
## Issue Workflow
When working on an issue:
1. Read the issue using `tea issues <number>` to understand requirements.
2. Create a feature branch following the branch naming convention.
3. Make commits following the conventional commit format.
4. Submit a pull request when ready.
**Important**: Never commit directly to `main`.
All work must be done in a feature branch and submitted via pull request.
## Pull Request Management ## Pull Request Management
Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`. Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
@@ -52,3 +77,29 @@ Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
**Note**: Use `--description` (not `--body`) for PR body content. **Note**: Use `--description` (not `--body`) for PR body content.
**Creating PRs**: Always create PRs in a branch other than `main`, to the `main` branch unless specified otherwise. ALWAYS FOLLOW THE PR TEMPLATE, EXACTLY. **Creating PRs**: Always create PRs in a branch other than `main`, to the `main` branch unless specified otherwise. ALWAYS FOLLOW THE PR TEMPLATE, EXACTLY.
**Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`.
This automatically links and closes the issue when the PR is merged.
### Updating PRs
When pushing additional changes to an existing PR, add a comment summarizing the new commits.
This keeps reviewers informed of what changed since the initial PR description.
Use the `tea` CLI to add comments to pull requests:
```bash
tea comment <number> "Comment text"
```
#### Examples
```bash
# Add a comment to PR #42
tea comment 42 "Updated implementation based on feedback"
# Add a multi-line comment
tea comment 42 "Summary of changes:
- Fixed bug in reducer
- Added new tests"
```

View File

@@ -48,7 +48,7 @@ The "source code" for a work means the preferred form of the work for making mod
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code runtime used to run it.
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
subprograms and other parts of the work. subprograms and other parts of the work.

View File

@@ -1,19 +1,21 @@
BINARY_NAME=lambda BINARY_NAME=lambda
TEST=simple TEST=simple
.PHONY: help build run profile explain graph docs bench clean .PHONY: help build run profile explain graph docs test bench lint clean
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.SILENT: .SILENT:
help: help:
echo "Available targets:" echo "Available targets:"
echo " build - Build the lambda executable" echo " build - Build the lambda executable"
echo " run - Build and run the lambda interpreter (use TEST=<name> to specify sample)" echo " run - Build and run the lambda runtime (use TEST=<name> to specify sample)"
echo " profile - Build and run with CPU profiling enabled" echo " profile - Build and run with CPU profiling enabled"
echo " explain - Build and run with explanation mode and profiling" echo " explain - Build and run with explanation mode and profiling"
echo " graph - Generate and open CPU profile visualization" echo " graph - Generate and open CPU profile visualization"
echo " docs - Start local godoc server on port 6060" echo " docs - Start local godoc server on port 6060"
echo " test - Run tests for all samples"
echo " bench - Run benchmarks for all samples" echo " bench - Run benchmarks for all samples"
echo " lint - Run golangci-lint on all packages"
echo " clean - Remove all build artifacts" echo " clean - Remove all build artifacts"
build: build:
@@ -38,9 +40,15 @@ docs:
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'" echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
go run golang.org/x/tools/cmd/godoc@latest -http=:6060 go run golang.org/x/tools/cmd/godoc@latest -http=:6060
test:
go test -v ./cmd/lambda
bench: bench:
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
lint:
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
clean: clean:
rm -f ${BINARY_NAME} rm -f ${BINARY_NAME}
rm -f program.out rm -f program.out

View File

@@ -1,6 +1,6 @@
# lambda # lambda
Making a lambda calculus interpreter in Go. Making a lambda calculus runtime in Go.
## Things to talk about ## Things to talk about

View File

@@ -1,70 +0,0 @@
package main
import (
"os"
"testing"
"git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/engine"
"git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
)
// Helper function to run a single sample through the lambda interpreter.
func runSample(samplePath string) error {
// Read the sample file.
input, err := os.ReadFile(samplePath)
if err != nil {
return err
}
// Parse code into syntax tree.
ast, err := saccharine.Parse(string(input))
if err != nil {
return err
}
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
// Create minimal config for benchmarking.
cfg := &config.Config{
Source: config.StringSource{Data: ""},
Destination: config.StdoutDestination{},
Profile: "",
Explanation: false,
Statistics: false,
Verbose: false,
}
// Create and run the engine.
process := engine.New(cfg, &compiled)
process.Run()
// Get final result (to ensure it's not optimized away).
_ = lambda.Stringify(compiled)
return nil
}
// Benchmark all samples using sub-benchmarks.
func BenchmarkSamples(b *testing.B) {
samples := map[string]string{
"Church": "../../tests/church.test",
"Fast": "../../tests/fast.test",
"Saccharine": "../../tests/saccharine.test",
"Simple": "../../tests/simple.test",
"Thunk": "../../tests/thunk.test",
}
for name, path := range samples {
b.Run(name, func(b *testing.B) {
for b.Loop() {
if err := runSample(path); err != nil {
b.Fatal(err)
}
}
})
}
}

View File

@@ -5,12 +5,9 @@ import (
"git.maximhutz.com/max/lambda/internal/cli" "git.maximhutz.com/max/lambda/internal/cli"
"git.maximhutz.com/max/lambda/internal/config" "git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/engine" "git.maximhutz.com/max/lambda/internal/plugins"
"git.maximhutz.com/max/lambda/internal/explanation"
"git.maximhutz.com/max/lambda/internal/performance"
"git.maximhutz.com/max/lambda/internal/statistics"
"git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/lambda" "git.maximhutz.com/max/lambda/pkg/normalorder"
"git.maximhutz.com/max/lambda/pkg/saccharine" "git.maximhutz.com/max/lambda/pkg/saccharine"
) )
@@ -34,44 +31,37 @@ func main() {
// Compile expression to lambda calculus. // Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast) compiled := convert.SaccharineToLambda(ast)
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled)) logger.Info("compiled λ expression", "tree", compiled.String())
// Create reduction engine. // Create reducer with the compiled expression.
process := engine.New(options, &compiled) runtime := normalorder.NewRuntime(compiled)
// If the user selected to track CPU performance, attach a profiler to the // If the user selected to track CPU performance, attach a profiler.
// process.
if options.Profile != "" { if options.Profile != "" {
profiler := performance.Track(options.Profile) plugins.NewPerformance(options.Profile, runtime)
process.On("start", profiler.Start)
process.On("end", profiler.End)
} }
// If the user selected to produce a step-by-step explanation, attach an // If the user selected to produce a step-by-step explanation, attach an
// observer here. // observer.
if options.Explanation { if options.Explanation {
explanation.Track(process) plugins.NewExplanation(runtime)
} }
// If the user opted to track statistics, attach a tracker here, too. // If the user opted to track statistics, attach a tracker.
if options.Statistics { if options.Statistics {
statistics := statistics.Track() plugins.NewStatistics(runtime)
process.On("start", statistics.Start)
process.On("step", statistics.Step)
process.On("end", statistics.End)
} }
// If the user selected for verbose debug logs, attach a reduction tracker. // If the user selected for verbose debug logs, attach a reduction tracker.
if options.Verbose { if options.Verbose {
process.On("step", func() { plugins.NewLogs(logger, runtime)
logger.Info("reduction", "tree", lambda.Stringify(compiled))
})
} }
process.Run() // Run reduction.
runtime.Run()
// Return the final reduced result. // Return the final reduced result.
result := lambda.Stringify(compiled) result := runtime.Expression().String()
err = options.Destination.Write(result) err = options.Destination.Write(result)
cli.HandleError(err) cli.HandleError(err)
} }

85
cmd/lambda/lambda_test.go Normal file
View File

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

26
cmd/lambda/registry.go Normal file
View File

@@ -0,0 +1,26 @@
package main
import (
"git.maximhutz.com/max/lambda/internal/cli"
"git.maximhutz.com/max/lambda/internal/registry"
"git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/engine/normalorder"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
)
func MakeRegistry() *registry.Registry {
r := registry.New()
// Codecs
r.AddCodec(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda"))
// Engines
r.AddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda"))
// Marshalers
r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda"))
r.AddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine"))
return r
}

8
go.mod
View File

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

9
go.sum Normal file
View File

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

55
internal/cli/codec.go Normal file
View File

@@ -0,0 +1,55 @@
package cli
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/codec"
)
type Codec interface {
codec.Codec[Repr, Repr]
InType() string
OutType() string
}
type convertedCodec[T, U any] struct {
codec codec.Codec[T, U]
inType, outType string
}
func (c convertedCodec[T, U]) Decode(r Repr) (Repr, error) {
u, ok := r.Data().(U)
if !ok {
return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType)
}
t, err := c.codec.Decode(u)
if err != nil {
return nil, err
}
return NewRepr(c.outType, t), nil
}
func (c convertedCodec[T, U]) Encode(r Repr) (Repr, error) {
t, ok := r.Data().(T)
if !ok {
return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.outType)
}
u, err := c.codec.Encode(t)
if err != nil {
return nil, err
}
return NewRepr(c.inType, u), nil
}
func (c convertedCodec[T, U]) InType() string { return c.inType }
func (c convertedCodec[T, U]) OutType() string { return c.outType }
func ConvertCodec[T, U any](e codec.Codec[T, U], inType, outType string) Codec {
return convertedCodec[T, U]{e, inType, outType}
}

49
internal/cli/engine.go Normal file
View File

@@ -0,0 +1,49 @@
package cli
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/engine"
)
type Engine interface {
engine.Engine[Repr]
Name() string
InType() string
}
type convertedEngine[T any] struct {
engine engine.Engine[T]
name string
inType string
}
func (b convertedEngine[T]) InType() string { return b.inType }
func (b convertedEngine[T]) Name() string { return b.name }
func (b convertedEngine[T]) Get() (Repr, error) {
s, err := b.engine.Get()
if err != nil {
return nil, err
}
return NewRepr(b.inType, s), nil
}
func (b convertedEngine[T]) Set(r Repr) error {
if t, ok := r.Data().(T); ok {
return b.engine.Set(t)
}
return fmt.Errorf("Incorrent format '%s' for engine '%s'.", r.Id(), b.inType)
}
func (b convertedEngine[T]) Step(i int) bool {
return b.engine.Step(i)
}
func ConvertEngine[T any](e engine.Engine[T], name string, inType string) Engine {
return convertedEngine[T]{e, name, inType}
}

42
internal/cli/marshaler.go Normal file
View File

@@ -0,0 +1,42 @@
package cli
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/codec"
)
type Marshaler interface {
codec.Marshaler[Repr]
InType() string
}
type convertedMarshaler[T any] struct {
codec codec.Marshaler[T]
inType string
}
func (c convertedMarshaler[T]) Decode(s string) (Repr, error) {
t, err := c.codec.Decode(s)
if err != nil {
return nil, err
}
return NewRepr(c.inType, t), nil
}
func (c convertedMarshaler[T]) Encode(r Repr) (string, error) {
t, ok := r.Data().(T)
if !ok {
return "", fmt.Errorf("could not parse '%v' as 'string'", t)
}
return c.codec.Encode(t)
}
func (c convertedMarshaler[T]) InType() string { return c.inType }
func ConvertMarshaler[T any](e codec.Marshaler[T], inType string) Marshaler {
return convertedMarshaler[T]{e, inType}
}

21
internal/cli/repr.go Normal file
View File

@@ -0,0 +1,21 @@
package cli
type Repr interface {
// Id returns to name of the objects underlying representation. If is
// assumed that if two Repr objects have the same Id(), they share the same
// representation.
Id() string
Data() any
}
type baseRepr struct {
id string
data any
}
func (r baseRepr) Id() string { return r.id }
func (r baseRepr) Data() any { return r.data }
func NewRepr(id string, data any) Repr { return baseRepr{id, data} }

View File

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

View File

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

View File

@@ -1,53 +0,0 @@
// 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()
}

View File

@@ -0,0 +1,75 @@
package registry
import (
"fmt"
"git.maximhutz.com/max/lambda/internal/cli"
)
type Registry struct {
marshalers map[string]cli.Marshaler
codecs []cli.Codec
engines map[string]cli.Engine
}
func New() *Registry {
return &Registry{
marshalers: map[string]cli.Marshaler{},
codecs: []cli.Codec{},
engines: map[string]cli.Engine{},
}
}
func (r *Registry) AddCodec(c cli.Codec) error {
r.codecs = append(r.codecs, c)
return nil
}
func (r *Registry) AddMarshaler(c cli.Marshaler) error {
if _, ok := r.marshalers[c.InType()]; ok {
return fmt.Errorf("marshaler for '%s' already registered", c.InType())
}
r.marshalers[c.InType()] = c
return nil
}
func (r *Registry) AddEngine(e cli.Engine) error {
if _, ok := r.engines[e.Name()]; ok {
return fmt.Errorf("engine '%s' already registered", e.Name())
}
r.engines[e.Name()] = e
return nil
}
func (r *Registry) GetEngine(name string) (cli.Engine, error) {
e, ok := r.engines[name]
if !ok {
return nil, fmt.Errorf("engine '%s' not found", name)
}
return e, nil
}
func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) {
panic("")
}
func (r *Registry) Marshal(repr cli.Repr) (string, error) {
m, ok := r.marshalers[repr.Id()]
if !ok {
return "", fmt.Errorf("no marshaler for '%s'", repr.Id())
}
return m.Encode(repr)
}
func (r *Registry) Unmarshal(s string, outType string) (cli.Repr, error) {
m, ok := r.marshalers[outType]
if !ok {
return nil, fmt.Errorf("no marshaler for '%s'", outType)
}
return m.Decode(s)
}

View File

@@ -1,28 +0,0 @@
// 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()
}

View File

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

8
pkg/codec/codec.go Normal file
View File

@@ -0,0 +1,8 @@
package codec
type Codec[T, U any] interface {
Encode(T) (U, error)
Decode(U) (T, error)
}
type Marshaler[T any] = Codec[T, string]

View File

@@ -3,23 +3,24 @@ 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 convertAtom(n *saccharine.Atom) lambda.Expression { func encodeAtom(n *saccharine.Atom) lambda.Expression {
return lambda.NewVariable(n.Name) return lambda.NewVariable(n.Name)
} }
func convertAbstraction(n *saccharine.Abstraction) lambda.Expression { func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression {
result := SaccharineToLambda(n.Body) result := encodeExpression(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 := lambda.GetFreeVariables(result) freeVars := result.GetFree()
freshName := lambda.GenerateFreshName(freeVars) freshName := lambda.GenerateFreshName(freeVars)
parameters = append(parameters, freshName) parameters = append(parameters, freshName)
} }
@@ -31,13 +32,13 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
return result return result
} }
func convertApplication(n *saccharine.Application) lambda.Expression { func encodeApplication(n *saccharine.Application) lambda.Expression {
result := SaccharineToLambda(n.Abstraction) result := encodeExpression(n.Abstraction)
arguments := []lambda.Expression{} arguments := []lambda.Expression{}
for _, argument := range n.Arguments { for _, argument := range n.Arguments {
convertedArgument := SaccharineToLambda(argument) encodeedArgument := encodeExpression(argument)
arguments = append(arguments, convertedArgument) arguments = append(arguments, encodeedArgument)
} }
for _, argument := range arguments { for _, argument := range arguments {
@@ -51,9 +52,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 = SaccharineToLambda(s.Body) value = encodeExpression(s.Body)
} else { } else {
value = convertAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body)) value = encodeAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
} }
return lambda.NewApplication( return lambda.NewApplication(
@@ -63,11 +64,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(lambda.GetFreeVariables(e)) freshVar := lambda.GenerateFreshName(e.GetFree())
return lambda.NewApplication( return lambda.NewApplication(
lambda.NewAbstraction(freshVar, e), lambda.NewAbstraction(freshVar, e),
SaccharineToLambda(s.Value), encodeExpression(s.Value),
) )
} }
@@ -82,8 +83,8 @@ func reduceStatement(s saccharine.Statement, e lambda.Expression) lambda.Express
} }
} }
func convertClause(n *saccharine.Clause) lambda.Expression { func encodeClause(n *saccharine.Clause) lambda.Expression {
result := SaccharineToLambda(n.Returns) result := encodeExpression(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)
@@ -92,17 +93,46 @@ func convertClause(n *saccharine.Clause) lambda.Expression {
return result return result
} }
func SaccharineToLambda(n saccharine.Expression) lambda.Expression { func encodeExpression(s saccharine.Expression) lambda.Expression {
switch n := n.(type) { switch s := s.(type) {
case *saccharine.Atom: case *saccharine.Atom:
return convertAtom(n) return encodeAtom(s)
case *saccharine.Abstraction: case *saccharine.Abstraction:
return convertAbstraction(n) return encodeAbstraction(s)
case *saccharine.Application: case *saccharine.Application:
return convertApplication(n) return encodeApplication(s)
case *saccharine.Clause: case *saccharine.Clause:
return convertClause(n) return encodeClause(s)
default: default:
panic(fmt.Errorf("unknown expression type: %T", n)) panic(fmt.Errorf("unknown expression type: %T", s))
} }
} }
func decodeExression(l lambda.Expression) saccharine.Expression {
switch l := l.(type) {
case lambda.Variable:
return saccharine.NewAtom(l.Name())
case lambda.Abstraction:
return saccharine.NewAbstraction(
[]string{l.Parameter()},
decodeExression(l.Body()))
case lambda.Application:
return saccharine.NewApplication(
decodeExression(l.Abstraction()),
[]saccharine.Expression{decodeExression(l.Argument())})
default:
panic(fmt.Errorf("unknown expression type: %T", l))
}
}
type Saccharine2Lambda struct{}
func (c Saccharine2Lambda) Decode(l lambda.Expression) (saccharine.Expression, error) {
return decodeExression(l), nil
}
func (c Saccharine2Lambda) Encode(s saccharine.Expression) (lambda.Expression, error) {
return encodeExpression(s), nil
}
var _ codec.Codec[saccharine.Expression, lambda.Expression] = (*Saccharine2Lambda)(nil)

View File

@@ -1,6 +0,0 @@
// Package "deltanet" is a reduction strategy using ∆-nets.
package deltanet
type Graph struct {
Nodes []Node
}

View File

@@ -1,94 +0,0 @@
package deltanet
/** ------------------------------------------------------------------------- */
// A connection between exactly two nodes in a graph.
type Edge struct {
A, B Node
}
// Returns all nodes the edge is connected to.
func (e Edge) GetConnections() []Node { return []Node{e.A, e.B} }
// Determines if a node is connected via this edge.
func (e Edge) IsConnected(n Node) bool { return e.A == n || e.B == n }
// Swaps an edges connected with one node, for another.
func (e *Edge) Swap(from Node, to Node) {
if e.A == from {
e.A = to
}
if e.B == from {
e.B = to
}
}
// Returns true if the edge is connected to each node via their pricniple ports.
func (e Edge) IsPrincipleEdge() bool {
return e.A.GetMainPort() == e && e.B.GetMainPort() == e
}
/** ------------------------------------------------------------------------- */
type Node interface {
// Returns the principle port that the node is attached to.
GetMainPort() Edge
// Returns all auxiliary ports that the node has. These ports are guaranteed
// to be ordered clockwise, as they would appear graphically.
GetAuxPorts() []Edge
// Returns the label of the node. May be blank.
GetLabel() string
}
/** ------------------------------------------------------------------------- */
type EraserNode struct {
Main Edge
}
func (n EraserNode) GetLabel() string { return "Ⓧ" }
func (n EraserNode) GetMainPort() Edge { return n.Main }
func (n EraserNode) GetAuxPorts() []Edge { return []Edge{} }
/** ------------------------------------------------------------------------- */
type ReplicatorNode struct {
Main Edge
Level uint
Aux []Edge
Deltas []int
}
func (n ReplicatorNode) GetLabel() string { return "" }
func (n ReplicatorNode) GetMainPort() Edge { return n.Main }
func (n ReplicatorNode) GetAuxPorts() []Edge { return n.Aux }
// Returns the level of the replicator node.
func (n ReplicatorNode) GetLevel() uint { return n.Level }
/** ------------------------------------------------------------------------- */
type FanNode struct {
Label string
Main Edge
Left, Right Edge
}
func (n FanNode) GetLabel() string { return n.Label }
func (n FanNode) GetMainPort() Edge { return n.Main }
func (n FanNode) GetAuxPorts() []Edge { return []Edge{n.Left, n.Right} }
/** ------------------------------------------------------------------------- */
type TerminalNode struct {
Label string
Main Edge
}
func (n TerminalNode) GetLabel() string { return n.Label }
func (n TerminalNode) GetMainPort() Edge { return n.Main }
func (n TerminalNode) GetAuxPorts() []Edge { return []Edge{} }
/** ------------------------------------------------------------------------- */

View File

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

19
pkg/emitter/listener.go Normal file
View File

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

7
pkg/engine/engine.go Normal file
View File

@@ -0,0 +1,7 @@
package engine
type Engine[T any] interface {
Get() (T, error)
Set(T) error
Step(int) bool
}

View File

@@ -0,0 +1,34 @@
package normalorder
import (
"git.maximhutz.com/max/lambda/pkg/engine"
"git.maximhutz.com/max/lambda/pkg/lambda"
)
type Engine struct {
expr lambda.Expression
}
func (e Engine) Get() (lambda.Expression, error) {
return e.expr, nil
}
func (e Engine) Set(l lambda.Expression) error {
e.expr = l
return nil
}
func (e Engine) Step(i int) bool {
var reduced bool
for range i {
e.expr, reduced = ReduceOnce(e.expr)
if !reduced {
return false
}
}
return true
}
var _ engine.Engine[lambda.Expression] = (*Engine)(nil)

View File

@@ -0,0 +1,34 @@
package normalorder
import "git.maximhutz.com/max/lambda/pkg/lambda"
func ReduceOnce(e lambda.Expression) (lambda.Expression, bool) {
switch e := e.(type) {
case lambda.Abstraction:
body, reduced := ReduceOnce(e.Body())
if reduced {
return lambda.NewAbstraction(e.Parameter(), body), true
}
return e, false
case lambda.Application:
if fn, fnOk := e.Abstraction().(lambda.Abstraction); fnOk {
return fn.Body().Substitute(fn.Parameter(), e.Argument()), true
}
abs, reduced := ReduceOnce(e.Abstraction())
if reduced {
return lambda.NewApplication(abs, e.Argument()), true
}
arg, reduced := ReduceOnce(e.Argument())
if reduced {
return lambda.NewApplication(e.Abstraction(), arg), true
}
return e, false
default:
return e, false
}
}

View File

@@ -1,7 +1,32 @@
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 {
Accept(Visitor) fmt.Stringer
// Substitute replaces all free occurrences of the target variable with the
// replacement expression. Alpha-renaming is performed automatically to
// avoid variable capture.
Substitute(target string, replacement Expression) Expression
// GetFree returns the set of all free variable names in the expression.
// This function does not mutate the input expression.
// The returned set is newly allocated and can be modified by the caller.
GetFree() set.Set[string]
// Rename replaces all occurrences of the target variable name with the new name.
Rename(target string, newName string) Expression
// IsFree returns true if the variable name n occurs free in the expression.
// This function does not mutate the input expression.
IsFree(n string) bool
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
@@ -11,20 +36,22 @@ type Abstraction struct {
body Expression body Expression
} }
func (a *Abstraction) Parameter() string { var _ Expression = Abstraction{}
func (a Abstraction) Parameter() string {
return a.parameter return a.parameter
} }
func (a *Abstraction) Body() Expression { func (a Abstraction) Body() Expression {
return a.body return a.body
} }
func (a *Abstraction) Accept(v Visitor) { func (a Abstraction) String() string {
v.VisitAbstraction(a) return "\\" + a.parameter + "." + a.body.String()
} }
func NewAbstraction(parameter string, body Expression) *Abstraction { func NewAbstraction(parameter string, body Expression) Abstraction {
return &Abstraction{parameter: parameter, body: body} return Abstraction{parameter, body}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
@@ -34,44 +61,40 @@ type Application struct {
argument Expression argument Expression
} }
func (a *Application) Abstraction() Expression { var _ Expression = Application{}
func (a Application) Abstraction() Expression {
return a.abstraction return a.abstraction
} }
func (a *Application) Argument() Expression { func (a Application) Argument() Expression {
return a.argument return a.argument
} }
func (a *Application) Accept(v Visitor) { func (a Application) String() string {
v.VisitApplication(a) return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
} }
func NewApplication(abstraction Expression, argument Expression) *Application { func NewApplication(abstraction Expression, argument Expression) Application {
return &Application{abstraction: abstraction, argument: argument} return Application{abstraction, argument}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
type Variable struct { type Variable struct {
value string name string
} }
func (v *Variable) Value() string { var _ Expression = Variable{}
return v.value
func (v Variable) Name() string {
return v.name
} }
func (v *Variable) Accept(visitor Visitor) { func (v Variable) String() string {
visitor.VisitVariable(v) return v.name
} }
func NewVariable(name string) *Variable { func NewVariable(name string) Variable {
return &Variable{value: name} return Variable{name}
}
/** ------------------------------------------------------------------------- */
type Visitor interface {
VisitAbstraction(*Abstraction)
VisitApplication(*Application)
VisitVariable(*Variable)
} }

View File

@@ -6,7 +6,9 @@ import (
"git.maximhutz.com/max/lambda/pkg/set" "git.maximhutz.com/max/lambda/pkg/set"
) )
func GenerateFreshName(used *set.Set[string]) string { // GenerateFreshName generates a variable name that is not in the used set.
// This function does not mutate the used set.
func GenerateFreshName(used set.Set[string]) string {
for i := uint64(0); ; i++ { for i := uint64(0); ; i++ {
attempt := "_" + string(strconv.AppendUint(nil, i, 10)) attempt := "_" + string(strconv.AppendUint(nil, i, 10))

View File

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

View File

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

View File

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

19
pkg/lambda/marshaler.go Normal file
View File

@@ -0,0 +1,19 @@
package lambda
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/codec"
)
type Marshaler struct{}
func (m Marshaler) Decode(string) (Expression, error) {
return nil, fmt.Errorf("unimplemented")
}
func (m Marshaler) Encode(e Expression) (string, error) {
return e.String(), nil
}
var _ codec.Marshaler[Expression] = (*Marshaler)(nil)

View File

@@ -1,30 +0,0 @@
package lambda
func IsViable(e *Expression) (*Abstraction, Expression, bool) {
if e == nil {
return nil, nil, false
} else if app, appOk := (*e).(*Application); !appOk {
return nil, nil, false
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
return nil, nil, false
} else {
return fn, app.argument, true
}
}
func ReduceAll(e *Expression, step func()) {
it := NewIterator(e)
for !it.Done() {
if fn, arg, ok := IsViable(it.Current()); !ok {
it.Next()
} else {
it.Swap(Substitute(fn.body, fn.parameter, arg))
step()
if _, _, ok := IsViable(it.Parent()); ok {
it.Back()
}
}
}
}

View File

@@ -1,38 +1,28 @@
package lambda package lambda
func Rename(expr Expression, target string, newName string) Expression { // Rename replaces all occurrences of the target variable name with the new name.
switch e := expr.(type) { func (e Variable) Rename(target string, newName string) Expression {
case *Variable: if e.Name() == target {
if e.value == target {
return NewVariable(newName) return NewVariable(newName)
} }
return e
case *Abstraction: return e
newParam := e.parameter }
if e.parameter == target {
func (e Abstraction) Rename(target string, newName string) Expression {
newParam := e.Parameter()
if e.Parameter() == target {
newParam = newName newParam = newName
} }
newBody := Rename(e.body, target, newName) newBody := e.Body().Rename(target, newName)
if newParam == e.parameter && newBody == e.body {
return e
}
return NewAbstraction(newParam, newBody) return NewAbstraction(newParam, newBody)
case *Application:
newAbs := Rename(e.abstraction, target, newName)
newArg := Rename(e.argument, target, newName)
if newAbs == e.abstraction && newArg == e.argument {
return e
} }
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) return NewApplication(newAbs, newArg)
default:
return expr
}
} }

View File

@@ -1,32 +0,0 @@
package lambda
import "strings"
type stringifyVisitor struct {
builder strings.Builder
}
func (v *stringifyVisitor) VisitVariable(a *Variable) {
v.builder.WriteString(a.value)
}
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
v.builder.WriteRune('\\')
v.builder.WriteString(f.parameter)
v.builder.WriteRune('.')
f.body.Accept(v)
}
func (v *stringifyVisitor) VisitApplication(c *Application) {
v.builder.WriteRune('(')
c.abstraction.Accept(v)
v.builder.WriteRune(' ')
c.argument.Accept(v)
v.builder.WriteRune(')')
}
func Stringify(e Expression) string {
b := &stringifyVisitor{builder: strings.Builder{}}
e.Accept(b)
return b.builder.String()
}

View File

@@ -1,46 +1,35 @@
package lambda package lambda
func Substitute(expr Expression, target string, replacement Expression) Expression { func (e Variable) Substitute(target string, replacement Expression) Expression {
switch e := expr.(type) { if e.Name() == target {
case *Variable:
if e.value == target {
return replacement return replacement
} }
return e
case *Abstraction:
if e.parameter == target {
return e return e
} }
body := e.body func (e Abstraction) Substitute(target string, replacement Expression) Expression {
param := e.parameter if e.Parameter() == target {
if IsFreeVariable(param, replacement) { return e
freeVars := GetFreeVariables(replacement) }
freeVars.Merge(GetFreeVariables(body))
body := e.Body()
param := e.Parameter()
if replacement.IsFree(param) {
freeVars := replacement.GetFree()
freeVars.Merge(body.GetFree())
freshVar := GenerateFreshName(freeVars) freshVar := GenerateFreshName(freeVars)
body = Rename(body, param, freshVar) body = body.Rename(param, freshVar)
param = freshVar param = freshVar
} }
newBody := Substitute(body, target, replacement) newBody := body.Substitute(target, replacement)
if newBody == body && param == e.parameter {
return e
}
return NewAbstraction(param, newBody) return NewAbstraction(param, newBody)
case *Application:
newAbs := Substitute(e.abstraction, target, replacement)
newArg := Substitute(e.argument, target, replacement)
if newAbs == e.abstraction && newArg == e.argument {
return e
} }
return NewApplication(newAbs, newArg) func (e Application) Substitute(target string, replacement Expression) Expression {
abs := e.Abstraction().Substitute(target, replacement)
arg := e.Argument().Substitute(target, replacement)
default: return NewApplication(abs, arg)
return expr
}
} }

View File

@@ -0,0 +1,24 @@
// Package "saccharine" provides a simple language built on top of λ-calculus,
// to facilitate productive coding using it.
package saccharine
import (
"git.maximhutz.com/max/lambda/pkg/codec"
)
type Marshaler struct{}
func (m Marshaler) Decode(s string) (Expression, error) {
tokens, err := scan(s)
if err != nil {
return nil, err
}
return parse(tokens)
}
func (m Marshaler) Encode(e Expression) (string, error) {
return stringifyExpression(e), nil
}
var _ codec.Marshaler[Expression] = (*Marshaler)(nil)

View File

@@ -5,18 +5,17 @@ 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.Token] type TokenIterator = iterator.Iterator[Token]
func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) { func parseRawToken(i *TokenIterator, expected TokenType) (*Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) { return iterator.Do(i, func(i *TokenIterator) (*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'", token.Name(expected), tok.Value) return nil, fmt.Errorf("expected token %v, got %v'", expected.Name(), tok.Value)
} else { } else {
return &tok, nil return &tok, nil
} }
@@ -25,14 +24,14 @@ func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error)
func passSoftBreaks(i *TokenIterator) { func passSoftBreaks(i *TokenIterator) {
for { for {
if _, err := parseRawToken(i, token.SoftBreak); err != nil { if _, err := parseRawToken(i, TokenSoftBreak); err != nil {
return return
} }
} }
} }
func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*token.Token, error) { func parseToken(i *TokenIterator, expected TokenType, ignoreSoftBreaks bool) (*Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) { return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
if ignoreSoftBreaks { if ignoreSoftBreaks {
passSoftBreaks(i) passSoftBreaks(i)
} }
@@ -42,17 +41,17 @@ func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*
} }
func parseString(i *TokenIterator) (string, error) { func parseString(i *TokenIterator) (string, error) {
if tok, err := parseToken(i, token.Atom, true); err != nil { if tok, err := parseToken(i, TokenAtom, true); err != nil {
return "", trace.Wrap(err, "no variable (col %d)", i.Index()) 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.Token, error) { func parseBreak(i *TokenIterator) (*Token, error) {
if tok, softErr := parseRawToken(i, token.SoftBreak); softErr == nil { if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == nil {
return tok, nil return tok, nil
} else if tok, hardErr := parseRawToken(i, token.HardBreak); hardErr == nil { } else if tok, hardErr := parseRawToken(i, TokenHardBreak); hardErr == nil {
return tok, nil return tok, nil
} else { } else {
return nil, errors.Join(softErr, hardErr) return nil, errors.Join(softErr, hardErr)
@@ -76,11 +75,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, token.Slash, true); err != nil { if _, err := parseToken(i, TokenSlash, 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, token.Dot, true); err != nil { } else if _, err = parseToken(i, TokenDot, true); err != nil {
return nil, trace.Wrap(err, "no function dot (col %d)", i.MustGet().Column) 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
@@ -92,11 +91,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, token.OpenParen, true); err != nil { if _, err := parseToken(i, TokenOpenParen, 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, token.CloseParen, true); err != nil { } else if _, err := parseToken(i, TokenCloseParen, true); err != nil {
return nil, trace.Wrap(err, "no closing brackets (col %d)", i.MustGet().Column) 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
@@ -105,7 +104,7 @@ func parseApplication(i *TokenIterator) (*Application, error) {
} }
func parseAtom(i *TokenIterator) (*Atom, error) { func parseAtom(i *TokenIterator) (*Atom, error) {
if tok, err := parseToken(i, token.Atom, true); err != nil { if tok, err := parseToken(i, TokenAtom, 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
@@ -133,7 +132,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, token.OpenBrace, true); err != nil { if _, err := parseToken(i, TokenOpenBrace, true); err != nil {
return nil, err return nil, err
} }
} }
@@ -152,7 +151,7 @@ func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
} }
if braces { if braces {
if _, err := parseToken(i, token.CloseBrace, true); err != nil { if _, err := parseToken(i, TokenCloseBrace, true); err != nil {
return nil, err return nil, err
} }
} }
@@ -165,13 +164,13 @@ func parseExpression(i *TokenIterator) (Expression, error) {
passSoftBreaks(i) passSoftBreaks(i)
switch peek := i.MustGet(); peek.Type { switch peek := i.MustGet(); peek.Type {
case token.OpenParen: case TokenOpenParen:
return parseApplication(i) return parseApplication(i)
case token.Slash: case TokenSlash:
return parseAbstraction(i) return parseAbstraction(i)
case token.Atom: case TokenAtom:
return parseAtom(i) return parseAtom(i)
case token.OpenBrace: case TokenOpenBrace:
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)
@@ -183,7 +182,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, token.Assign, true); err != nil { } else if _, err := parseToken(i, TokenAssign, 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
@@ -212,7 +211,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.Token) (Expression, error) { func parse(tokens []Token) (Expression, error) {
i := iterator.Of(tokens) i := iterator.Of(tokens)
exp, err := parseClause(i, false) exp, err := parseClause(i, false)

View File

@@ -1,22 +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/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)
}

View File

@@ -1,4 +1,4 @@
package token package saccharine
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 parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) { func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
i2 := i.Copy() i2 := i.Copy()
if r, err := i2.Next(); err != nil { if r, err := i2.Next(); err != nil {
@@ -27,7 +27,7 @@ func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, erro
} }
} }
func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) { func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
i2 := i.Copy() i2 := i.Copy()
if r, err := i2.Next(); err != nil { if r, err := i2.Next(); err != nil {
@@ -42,7 +42,7 @@ func parseCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
// Pulls the next token from an iterator over runes. If it cannot, it will // 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 getToken(i *iterator.Iterator[rune]) (*Token, error) { func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
index := i.Index() index := i.Index()
if i.Done() { if i.Done() {
@@ -56,54 +56,69 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
switch { switch {
case letter == '(': case letter == '(':
return NewOpenParen(index), nil return NewTokenOpenParen(index), nil
case letter == ')': case letter == ')':
return NewCloseParen(index), nil return NewTokenCloseParen(index), nil
case letter == '.': case letter == '.':
return NewDot(index), nil return NewTokenDot(index), nil
case letter == '\\': case letter == '\\':
return NewSlash(index), nil return NewTokenSlash(index), nil
case letter == '\n': case letter == '\n':
return NewSoftBreak(index), nil return NewTokenSoftBreak(index), nil
case letter == '{': case letter == '{':
return NewOpenBrace(index), nil return NewTokenOpenBrace(index), nil
case letter == '}': case letter == '}':
return NewCloseBrace(index), nil return NewTokenCloseBrace(index), nil
case letter == ':': case letter == ':':
if _, err := parseCharacter(i, '='); err != nil { if _, err := scanCharacter(i, '='); err != nil {
return nil, err return nil, err
} else { } else {
return NewAssign(index), nil return NewTokenAssign(index), nil
} }
case letter == ';': case letter == ';':
return NewHardBreak(index), nil return NewTokenHardBreak(index), nil
case letter == '#':
// Skip everything until the next newline or EOF.
for !i.Done() {
r, err := i.Next()
if err != nil {
return nil, trace.Wrap(err, "error while parsing comment")
}
if r == '\n' {
// Put the newline back so it can be processed as a soft break.
i.Back()
break
}
}
return nil, nil
case unicode.IsSpace(letter): 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 := parseRune(i, isVariable); err != nil { if r, err := scanRune(i, isVariable); err != nil {
break break
} else { } else {
atom = append(atom, r) atom = append(atom, r)
} }
} }
return NewAtom(string(atom), index), nil return NewTokenAtom(string(atom), index), nil
} }
return nil, fmt.Errorf("unknown character '%v'", string(letter)) return nil, fmt.Errorf("unknown character '%v'", string(letter))
} }
// Parse a string into tokens. // scan a string into tokens.
func Parse(input string) ([]Token, error) { func scan(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 := getToken(i) token, err := scanToken(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.go Normal file
View File

@@ -0,0 +1,91 @@
package saccharine
import "fmt"
// All tokens in the pseudo-lambda language.
type TokenType int
const (
TokenOpenParen TokenType = iota // Denotes the '(' token.
TokenCloseParen // Denotes the ')' token.
TokenOpenBrace // Denotes the '{' token.
TokenCloseBrace // Denotes the '}' token.
TokenHardBreak // Denotes the ';' token.
TokenAssign // Denotes the ':=' token.
TokenAtom // Denotes an alpha-numeric variable.
TokenSlash // Denotes the '/' token.
TokenDot // Denotes the '.' token.
TokenSoftBreak // Denotes a new-line.
)
// A representation of a token in source code.
type Token struct {
Column int // Where the token begins in the source text.
Type TokenType // What type the token is.
Value string // The value of the token.
}
func NewTokenOpenParen(column int) *Token {
return &Token{Type: TokenOpenParen, Column: column, Value: "("}
}
func NewTokenCloseParen(column int) *Token {
return &Token{Type: TokenCloseParen, Column: column, Value: ")"}
}
func NewTokenOpenBrace(column int) *Token {
return &Token{Type: TokenOpenBrace, Column: column, Value: "{"}
}
func NewTokenCloseBrace(column int) *Token {
return &Token{Type: TokenCloseBrace, Column: column, Value: "}"}
}
func NewTokenDot(column int) *Token {
return &Token{Type: TokenDot, Column: column, Value: "."}
}
func NewTokenHardBreak(column int) *Token {
return &Token{Type: TokenHardBreak, Column: column, Value: ";"}
}
func NewTokenAssign(column int) *Token {
return &Token{Type: TokenAssign, Column: column, Value: ":="}
}
func NewTokenSlash(column int) *Token {
return &Token{Type: TokenSlash, Column: column, Value: "\\"}
}
func NewTokenAtom(name string, column int) *Token {
return &Token{Type: TokenAtom, Column: column, Value: name}
}
func NewTokenSoftBreak(column int) *Token {
return &Token{Type: TokenSoftBreak, Column: column, Value: "\\n"}
}
func (t TokenType) Name() string {
switch t {
case TokenOpenParen:
return "("
case TokenCloseParen:
return ")"
case TokenSlash:
return "\\"
case TokenDot:
return "."
case TokenAtom:
return "ATOM"
case TokenSoftBreak:
return "\\n"
case TokenHardBreak:
return ";"
default:
panic(fmt.Errorf("unknown token type %v", t))
}
}
func (t Token) Name() string {
return t.Type.Name()
}

View File

@@ -1,91 +0,0 @@
package token
import "fmt"
// All tokens in the pseudo-lambda language.
type Type int
const (
OpenParen Type = iota // Denotes the '(' token.
CloseParen // Denotes the ')' token.
OpenBrace // Denotes the '{' token.
CloseBrace // Denotes the '}' token.
HardBreak // Denotes the ';' token.
Assign // Denotes the ':=' token.
Atom // Denotes an alpha-numeric variable.
Slash // Denotes the '/' token.
Dot // Denotes the '.' token.
SoftBreak // Denotes a new-line.
)
// A representation of a token in source code.
type Token struct {
Column int // Where the token begins in the source text.
Type Type // What type the token is.
Value string // The value of the token.
}
func NewOpenParen(column int) *Token {
return &Token{Type: OpenParen, Column: column, Value: "("}
}
func NewCloseParen(column int) *Token {
return &Token{Type: CloseParen, Column: column, Value: ")"}
}
func NewOpenBrace(column int) *Token {
return &Token{Type: OpenBrace, Column: column, Value: "{"}
}
func NewCloseBrace(column int) *Token {
return &Token{Type: CloseBrace, Column: column, Value: "}"}
}
func NewDot(column int) *Token {
return &Token{Type: Dot, Column: column, Value: "."}
}
func NewHardBreak(column int) *Token {
return &Token{Type: HardBreak, Column: column, Value: ";"}
}
func NewAssign(column int) *Token {
return &Token{Type: Assign, Column: column, Value: ":="}
}
func NewSlash(column int) *Token {
return &Token{Type: Slash, Column: column, Value: "\\"}
}
func NewAtom(name string, column int) *Token {
return &Token{Type: Atom, Column: column, Value: name}
}
func NewSoftBreak(column int) *Token {
return &Token{Type: SoftBreak, Column: column, Value: "\\n"}
}
func Name(typ Type) string {
switch typ {
case OpenParen:
return "("
case CloseParen:
return ")"
case Slash:
return "\\"
case Dot:
return "."
case Atom:
return "ATOM"
case SoftBreak:
return "\\n"
case HardBreak:
return ";"
default:
panic(fmt.Errorf("unknown token type %v", typ))
}
}
func (t Token) Name() string {
return Name(t.Type)
}

View File

@@ -1,10 +1,12 @@
package set package set
import "iter"
type Set[T comparable] map[T]bool type Set[T comparable] map[T]bool
func (s *Set[T]) Add(items ...T) { func (s Set[T]) Add(items ...T) {
for _, item := range items { for _, item := range items {
(*s)[item] = true s[item] = true
} }
} }
@@ -12,14 +14,14 @@ func (s Set[T]) Has(item T) bool {
return s[item] return s[item]
} }
func (s *Set[T]) Remove(items ...T) { func (s Set[T]) Remove(items ...T) {
for _, item := range items { for _, item := range items {
delete(*s, item) delete(s, item)
} }
} }
func (s *Set[T]) Merge(o *Set[T]) { func (s Set[T]) Merge(o Set[T]) {
for item := range *o { for item := range o {
s.Add(item) s.Add(item)
} }
} }
@@ -34,8 +36,18 @@ func (s Set[T]) ToList() []T {
return list return list
} }
func New[T comparable](items ...T) *Set[T] { func (s Set[T]) Items() iter.Seq[T] {
result := &Set[T]{} return func(yield func(T) bool) {
for item := range s {
if !yield(item) {
return
}
}
}
}
func New[T comparable](items ...T) Set[T] {
result := Set[T]{}
for _, item := range items { for _, item := range items {
result.Add(item) result.Add(item)

File diff suppressed because one or more lines are too long

View File

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

1
tests/comments.expected Normal file
View File

@@ -0,0 +1 @@
VALUE

17
tests/comments.test Normal file
View File

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

View File

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

1
tests/list_2^30.expected Normal file
View File

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

View File

@@ -1,16 +0,0 @@
(\0.
(\inc.
(\add.
(\mult.
(\exp.
(exp (inc (inc (inc (inc 0)))) (inc (inc (inc (inc (inc 0))))))
\n m.(m n)
)
\m n f.(m (n f))
)
\n m.(m inc n)
)
\n f x.(f (n f x))
)
\f x.x
)

1
tests/thunk.expected Normal file
View File

@@ -0,0 +1 @@
VALUE