6 Commits

Author SHA1 Message Date
7054442dc6 feat: new registry 2026-02-06 23:19:27 -05:00
a3ee34732e refactor: rewrite CLI and internal architecture (#41)
## Description

The old architecture used a monolithic `main()` with a custom arg parser, an event-emitter-based runtime, and a plugin system for optional features.
This PR rewrites the CLI and internal architecture to be modular, extensible, and built around a registry of interchangeable components.

- Replace custom CLI arg parsing with Cobra subcommands (`convert`, `reduce`, `engine list`).
- Introduce a registry system (`internal/registry`) for marshalers, codecs, and engines, with BFS-based conversion path resolution.
- Add type-erased adapter layer (`internal/cli`) with `Repr`, `Engine`, `Process`, `Marshaler`, and `Conversion` interfaces wrapping generic `pkg/` types.
- Replace the event-emitter-based `Runtime` with a simpler `Engine`/`Process` model (`pkg/engine`).
- Add generic `Codec[T, U]` and `Marshaler[T]` interfaces (`pkg/codec`).
- Merge `saccharine/token` sub-package into `saccharine` and rename scanner functions from `parse*` to `scan*`.
- Make saccharine-to-lambda conversion bidirectional (encode and decode).
- Add `lambda.Marshaler` and `saccharine.Marshaler` implementing `codec.Marshaler`.
- Remove old infrastructure: `pkg/runtime`, `pkg/expr`, `internal/plugins`, `internal/statistics`.
- Add `make lint` target and update golangci-lint config.

### Decisions

- Cobra was chosen for the CLI framework to support nested subcommands and standard flag handling.
- The registry uses BFS to find conversion paths between representations, allowing multi-hop conversions without hardcoding routes.
- Type erasure via `cli.Repr` (wrapping `any`) enables the registry to work with heterogeneous types while keeping `pkg/` generics type-safe.
- The old plugin/event system was removed entirely rather than adapted, since the new `Process` model can support hooks differently in the future.

## Benefits

- Subcommands make the CLI self-documenting and easier to extend with new functionality.
- The registry pattern decouples representations, conversions, and engines, making it trivial to add new ones.
- BFS conversion routing means adding a single codec automatically enables transitive conversions.
- Simpler `Engine`/`Process` model reduces complexity compared to the event-emitter runtime.
- Consolidating the `token` sub-package reduces import depth and package sprawl.

## Checklist

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

Reviewed-on: #41
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
2026-02-07 03:25:32 +00: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
61 changed files with 1224 additions and 869 deletions

View File

@@ -1,6 +1,6 @@
--- ---
name: "Bug Report" name: "Bug Report"
about: "Report a bug or unexpected behavior in the lambda interpreter." about: "Report a bug or unexpected behavior in the lambda runtime."
title: "fix: " title: "fix: "
ref: "main" ref: "main"
assignees: [] assignees: []

View File

@@ -1,6 +1,6 @@
--- ---
name: "Feature Request" name: "Feature Request"
about: "Suggest a new feature or enhancement for the lambda interpreter." about: "Suggest a new feature or enhancement for the lambda runtime."
title: "feat: " title: "feat: "
ref: "main" ref: "main"
assignees: [] assignees: []

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

@@ -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,20 +1,21 @@
BINARY_NAME=lambda BINARY_NAME=lambda
TEST=simple TEST=simple
.PHONY: help build run profile explain graph docs test 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 " 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:
@@ -45,6 +46,9 @@ test:
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

@@ -3,65 +3,29 @@ package main
import ( import (
"os" "os"
"git.maximhutz.com/max/lambda/internal/cli" "github.com/spf13/cobra"
"git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/plugins"
"git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
) )
func main() { func Lambda() *cobra.Command {
// Parse CLI arguments. cmd := &cobra.Command{
options, err := config.FromArgs() Use: "lambda",
cli.HandleError(err) Short: "Lambda calculus interpreter",
Long: "A lambda calculus interpreter supporting multiple representations.",
logger := options.GetLogger() RunE: func(cmd *cobra.Command, _ []string) error {
logger.Info("using program arguments", "args", os.Args) return cmd.Help()
logger.Info("parsed CLI options", "options", options) },
// Get input.
input, err := options.Source.Extract()
cli.HandleError(err)
// Parse code into syntax tree.
ast, err := saccharine.Parse(input)
cli.HandleError(err)
logger.Info("parsed syntax tree", "tree", ast)
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
logger.Info("compiled λ expression", "tree", compiled.String())
// Create reducer with the compiled expression.
reducer := lambda.NewNormalOrderReducer(&compiled)
// If the user selected to track CPU performance, attach a profiler.
if options.Profile != "" {
plugins.NewPerformance(options.Profile, reducer)
} }
// If the user selected to produce a step-by-step explanation, attach an cmd.AddCommand(LambdaConvert())
// observer. cmd.AddCommand(LambdaEngine())
if options.Explanation { cmd.AddCommand(LambdaReduce())
plugins.NewExplanation(reducer)
}
// If the user opted to track statistics, attach a tracker. return cmd
if options.Statistics { }
plugins.NewStatistics(reducer)
} func main() {
lambda := Lambda()
// If the user selected for verbose debug logs, attach a reduction tracker. if err := lambda.Execute(); err != nil {
if options.Verbose { os.Exit(1)
plugins.NewLogs(logger, reducer) }
}
// Run reduction.
reducer.Reduce()
// Return the final reduced result.
result := reducer.Expression().String()
err = options.Destination.Write(result)
cli.HandleError(err)
} }

View File

@@ -0,0 +1,94 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
// inferReprFromPath returns the repr type based on file extension.
func inferReprFromPath(path string) (string, error) {
switch ext := strings.ToLower(filepath.Ext(path)); ext {
case ".lambda", ".lam", ".lc":
return "lambda", nil
case ".saccharine", ".sch":
return "saccharine", nil
default:
return "", fmt.Errorf("unknown file extension '%s'", ext)
}
}
func LambdaConvert() *cobra.Command {
var inputReprFlag, outputReprFlag string
cmd := &cobra.Command{
Use: "convert <input-file> <output-file>",
Short: "Convert between lambda calculus representations",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return cmd.Help()
}
var err error
inputPath, outputPath := args[0], args[1]
// Use flag if provided, otherwise infer from extension.
inputRepr := inputReprFlag
if inputRepr == "" {
if inputRepr, err = inferReprFromPath(inputPath); err != nil {
return fmt.Errorf("input file: %w", err)
}
}
outputRepr := outputReprFlag
if outputRepr == "" {
if outputRepr, err = inferReprFromPath(outputPath); err != nil {
return fmt.Errorf("output file: %w", err)
}
}
// Read input file.
input, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("reading input file: %w", err)
}
r := GetRegistry()
// Parse input into syntax tree.
repr, err := r.Unmarshal(string(input), inputRepr)
if err != nil {
return fmt.Errorf("parsing input: %w", err)
}
// Convert to output repr if different.
result, err := r.ConvertTo(repr, outputRepr)
if err != nil {
return fmt.Errorf("converting %s to %s: %w", inputRepr, outputRepr, err)
}
// Marshal output.
output, err := r.Marshal(result)
if err != nil {
return fmt.Errorf("unmarshaling output: %w", err)
}
// Write output file.
err = os.WriteFile(outputPath, []byte(output), 0644)
if err != nil {
return fmt.Errorf("writing output file: %w", err)
}
return nil
},
}
cmd.Flags().StringVar(&inputReprFlag, "from", "", "Input representation (inferred from extension if unset)")
cmd.Flags().StringVar(&outputReprFlag, "to", "", "Output representation (inferred from extension if unset)")
return cmd
}

View File

@@ -0,0 +1,19 @@
package main
import (
"github.com/spf13/cobra"
)
func LambdaEngine() *cobra.Command {
cmd := &cobra.Command{
Use: "engine",
Short: "Information about available engines",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(LambdaEngineList())
return cmd
}

View File

@@ -0,0 +1,26 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func LambdaEngineList() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List available engines",
RunE: func(cmd *cobra.Command, args []string) error {
r := GetRegistry()
for engine := range r.ListEngines() {
fmt.Println(engine.Name())
}
return nil
},
}
return cmd
}

109
cmd/lambda/lambda_reduce.go Normal file
View File

@@ -0,0 +1,109 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
"git.maximhutz.com/max/lambda/internal/cli"
"git.maximhutz.com/max/lambda/internal/config"
)
func LambdaReduce() *cobra.Command {
var inputReprFlag string
var engineFlag string
cmd := &cobra.Command{
Use: "reduce <input-file>",
Short: "Reduce a lambda calculus expression",
SilenceUsage: true,
Aliases: []string{"run"},
RunE: func(cmd *cobra.Command, args []string) error {
var err error
if len(args) != 1 {
return cmd.Help()
}
inputPath := args[0]
// Get input source.
var source config.Source
if inputPath == "-" {
source = config.StdinSource{}
} else {
source = config.FileSource{Path: inputPath}
}
destination := config.StdoutDestination{}
r := GetRegistry()
// Get input.
input, err := source.Extract()
if err != nil {
return err
}
// Use flag if provided, otherwise infer from extension.
inputRepr := inputReprFlag
if inputRepr == "" {
if inputRepr, err = inferReprFromPath(inputPath); err != nil {
return fmt.Errorf("input file: %w", err)
}
}
// Find engine.
var engine cli.Engine
if engineFlag == "" {
if engine, err = r.GetDefaultEngine(inputRepr); err != nil {
return err
}
} else {
if engine, err = r.GetEngine(engineFlag); err != nil {
return err
}
}
// Parse code into syntax tree.
repr, err := r.Unmarshal(input, inputRepr)
if err != nil {
return err
}
// Compile expression to lambda calculus.
compiled, err := r.ConvertTo(repr, "lambda")
if err != nil {
return err
}
// Create process.
process := engine.Load()
err = process.Set(compiled)
if err != nil {
return err
}
// Run reduction.
for process.Step(1) {
}
// Return the final reduced result.
result, err := process.Get()
if err != nil {
return err
}
output, err := r.Marshal(result)
if err != nil {
return err
}
return destination.Write(output)
},
}
cmd.Flags().StringVar(&inputReprFlag, "from", "", "Input representation (inferred from extension if unset)")
cmd.Flags().StringVarP(&engineFlag, "engine", "e", "", "Reduction engine (inferred from '--input' if unset)")
return cmd
}

View File

@@ -1,85 +0,0 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
"github.com/stretchr/testify/assert"
)
// Helper function to run a single sample through the lambda interpreter.
func runSample(samplePath string) (string, error) {
// Read the sample file.
input, err := os.ReadFile(samplePath)
if err != nil {
return "", err
}
// Parse code into syntax tree.
ast, err := saccharine.Parse(string(input))
if err != nil {
return "", err
}
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
// Create and run the reducer.
reducer := lambda.NewNormalOrderReducer(&compiled)
reducer.Reduce()
return reducer.Expression().String() + "\n", nil
}
// 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 GetRegistry() *registry.Registry {
r := registry.New()
// Codecs
r.MustAddConversions(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda")...)
// Engines
r.MustAddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda"))
// Marshalers
r.MustAddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda"))
r.MustAddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine"))
return r
}

7
go.mod
View File

@@ -2,10 +2,9 @@ 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/spf13/cobra v1.10.2
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

18
go.sum
View File

@@ -1,9 +1,11 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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=

View File

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

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

@@ -0,0 +1,27 @@
package cli
import "git.maximhutz.com/max/lambda/pkg/engine"
type Engine interface {
Load() Process
Name() string
InType() string
}
type convertedEngine[T any] struct {
engine engine.Engine[T]
name string
inType string
}
func (e convertedEngine[T]) InType() string { return e.inType }
func (e convertedEngine[T]) Name() string { return e.name }
func (e convertedEngine[T]) Load() Process {
return convertedProcess[T]{e.engine.Load(), e.inType}
}
func ConvertEngine[T any](e engine.Engine[T], name, inType string) Engine {
return &convertedEngine[T]{e, name, inType}
}

View File

@@ -1,18 +0,0 @@
// Package "cli" provides miscellaneous helper functions.
package cli
import (
"fmt"
"os"
)
// A helper function to handle errors in the program. If it is given an error,
// the program will exist, and print the error.
func HandleError(err error) {
if err == nil {
return
}
fmt.Fprintln(os.Stderr, "ERROR:", err)
os.Exit(1)
}

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

@@ -0,0 +1,45 @@
package cli
import (
"fmt"
"reflect"
"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 {
dataType := reflect.TypeOf(r.Data())
allowedType := reflect.TypeFor[T]()
return "", fmt.Errorf("marshaler for '%s' cannot parse '%s'", allowedType, dataType)
}
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}
}

41
internal/cli/process.go Normal file
View File

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

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,23 +0,0 @@
package plugins
import (
"log/slog"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
type Logs struct {
logger *slog.Logger
reducer reducer.Reducer
}
func NewLogs(logger *slog.Logger, r reducer.Reducer) *Logs {
plugin := &Logs{logger, r}
r.On(reducer.StepEvent, plugin.Step)
return plugin
}
func (t *Logs) Step() {
t.logger.Info("reduction", "tree", t.reducer.Expression().String())
}

View File

@@ -1,31 +0,0 @@
// Package "explanation" provides an observer to gather the reasoning during the
// reduction, and present a thorough explanation to the user for each step.
package plugins
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// Track the reductions made by a reduction process.
type Explanation struct {
reducer reducer.Reducer
}
// Attaches a new explanation tracker to a reducer.
func NewExplanation(r reducer.Reducer) *Explanation {
plugin := &Explanation{reducer: r}
r.On(reducer.StartEvent, plugin.Start)
r.On(reducer.StepEvent, plugin.Step)
return plugin
}
func (t *Explanation) Start() {
fmt.Println(t.reducer.Expression().String())
}
func (t *Explanation) Step() {
fmt.Println(" =", t.reducer.Expression().String())
}

View File

@@ -1,59 +0,0 @@
// Package "performance" provides a tracker to observer CPU performance during
// execution.
package plugins
import (
"os"
"path/filepath"
"runtime/pprof"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// Observes a reduction process, and publishes a CPU performance profile on
// completion.
type Performance struct {
File string
filePointer *os.File
Error error
}
// Create a performance tracker that outputs a profile to "file".
func NewPerformance(file string, process reducer.Reducer) *Performance {
plugin := &Performance{File: file}
process.On(reducer.StartEvent, plugin.Start)
process.On(reducer.StopEvent, plugin.Stop)
return plugin
}
// Begin profiling.
func (t *Performance) 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 *Performance) Stop() {
pprof.StopCPUProfile()
t.filePointer.Close()
}

View File

@@ -1,44 +0,0 @@
package plugins
import (
"fmt"
"os"
"time"
"git.maximhutz.com/max/lambda/internal/statistics"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// An observer, to track reduction performance.
type Statistics struct {
start time.Time
steps uint64
}
// Create a new reduction performance Statistics.
func NewStatistics(r reducer.Reducer) *Statistics {
plugin := &Statistics{}
r.On(reducer.StartEvent, plugin.Start)
r.On(reducer.StepEvent, plugin.Step)
r.On(reducer.StopEvent, plugin.Stop)
return plugin
}
func (t *Statistics) Start() {
t.start = time.Now()
t.steps = 0
}
func (t *Statistics) Step() {
t.steps++
}
func (t *Statistics) Stop() {
results := statistics.Results{
StepsTaken: t.steps,
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
}
fmt.Fprint(os.Stderr, results.String())
}

View File

@@ -0,0 +1,27 @@
package registry
import (
"git.maximhutz.com/max/lambda/internal/cli"
)
type Converter struct {
data map[string][]cli.Conversion
}
func NewConverter() *Converter {
return &Converter{data: map[string][]cli.Conversion{}}
}
func (g *Converter) Add(c cli.Conversion) {
conversionsFromIn, ok := g.data[c.InType()]
if !ok {
conversionsFromIn = []cli.Conversion{}
}
conversionsFromIn = append(conversionsFromIn, c)
g.data[c.InType()] = conversionsFromIn
}
func (g *Converter) ConversionsFrom(t string) []cli.Conversion {
return g.data[t]
}

View File

@@ -0,0 +1,173 @@
package registry
import (
"fmt"
"iter"
"maps"
"git.maximhutz.com/max/lambda/internal/cli"
)
type Registry struct {
marshalers map[string]cli.Marshaler
converter *Converter
engines map[string]cli.Engine
}
func New() *Registry {
return &Registry{
marshalers: map[string]cli.Marshaler{},
converter: NewConverter(),
engines: map[string]cli.Engine{},
}
}
func (r *Registry) AddConversions(conversions ...cli.Conversion) error {
for _, conversion := range conversions {
r.converter.Add(conversion)
}
return nil
}
func (r *Registry) MustAddConversions(conversions ...cli.Conversion) {
if err := r.AddConversions(conversions...); err != nil {
panic(err)
}
}
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) MustAddMarshaler(c cli.Marshaler) {
if err := r.AddMarshaler(c); err != nil {
panic(err)
}
}
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) MustAddEngine(e cli.Engine) {
if err := r.AddEngine(e); err != nil {
panic(err)
}
}
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) ListEngines() iter.Seq[cli.Engine] {
return maps.Values(r.engines)
}
func (r *Registry) GetDefaultEngine(id string) (cli.Engine, error) {
for _, engine := range r.engines {
if engine.InType() == id {
return engine, nil
}
}
return nil, fmt.Errorf("no engine for '%s'", id)
}
func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) {
path, err := r.ConversionPath(repr.Id(), outType)
if err != nil {
return nil, err
}
result := repr
for _, conversion := range path {
result, err = conversion.Run(result)
if err != nil {
return nil, fmt.Errorf("converting '%s' to '%s': %w", conversion.InType(), conversion.OutType(), err)
}
}
return result, err
}
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)
}
func reverse[T any](list []T) []T {
if list == nil {
return list
}
reversed := []T{}
for i := len(list) - 1; i >= 0; i-- {
reversed = append(reversed, list[i])
}
return reversed
}
func (r *Registry) ConversionPath(from, to string) ([]cli.Conversion, error) {
backtrack := map[string]cli.Conversion{}
iteration := []string{from}
for len(iteration) > 0 {
nextIteration := []string{}
for _, item := range iteration {
for _, conversion := range r.converter.ConversionsFrom(item) {
if _, ok := backtrack[conversion.OutType()]; ok {
continue
}
nextIteration = append(nextIteration, conversion.OutType())
backtrack[conversion.OutType()] = conversion
}
}
iteration = nextIteration
}
reversedPath := []cli.Conversion{}
current := to
for current != from {
conversion, ok := backtrack[current]
if !ok {
return nil, fmt.Errorf("no valid conversion from '%s' to '%s'", from, to)
}
reversedPath = append(reversedPath, conversion)
current = conversion.InType()
}
return reverse(reversedPath), nil
}

View File

@@ -0,0 +1,9 @@
package registrynew
// Conversion
type Conversion interface {
InRepr() string
OutRepr() string
Run(Expr) (Expr, error)
}

View File

@@ -0,0 +1,12 @@
package registrynew
type Process interface {
Get() (Expr, error)
Step(int) bool
}
type Engine interface {
Load(Expr) Process
Name() string
InRepr() string
}

View File

@@ -0,0 +1,7 @@
package registrynew
type Expr interface {
Repr() string
Data() any
}

View File

@@ -0,0 +1,5 @@
package registrynew
type Marshaler interface {
InType() string
}

View File

@@ -0,0 +1,10 @@
package registrynew
import "iter"
type Registry interface {
ListEngines() iter.Seq[string]
GetEngine(name string) (Engine, error)
ListReprs() iter.Seq[string]
}

View File

@@ -0,0 +1,4 @@
package registrynew
type Repr interface {
}

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()
}

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

@@ -9,7 +9,7 @@ type Emitter[E comparable] interface {
} }
type BaseEmitter[E comparable] struct { type BaseEmitter[E comparable] struct {
listeners map[E]*set.Set[Listener[E]] listeners map[E]set.Set[Listener[E]]
} }
func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] { func (e *BaseEmitter[E]) On(kind E, fn func()) Listener[E] {
@@ -41,6 +41,6 @@ func (e *BaseEmitter[E]) Emit(event E) {
func New[E comparable]() *BaseEmitter[E] { func New[E comparable]() *BaseEmitter[E] {
return &BaseEmitter[E]{ return &BaseEmitter[E]{
listeners: map[E]*set.Set[Listener[E]]{}, listeners: map[E]set.Set[Listener[E]]{},
} }
} }

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

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

View File

@@ -0,0 +1,42 @@
package normalorder
import (
"git.maximhutz.com/max/lambda/pkg/engine"
"git.maximhutz.com/max/lambda/pkg/lambda"
)
type Process struct {
expr lambda.Expression
}
func (e Process) Get() (lambda.Expression, error) {
return e.expr, nil
}
func (e *Process) Set(l lambda.Expression) error {
e.expr = l
return nil
}
func (e *Process) Step(i int) bool {
for range i {
next, reduced := ReduceOnce(e.expr)
if !reduced {
return false
}
e.expr = next
}
return true
}
type Engine struct {
}
func (e Engine) Load() engine.Process[lambda.Expression] {
return &Process{}
}
var _ engine.Process[lambda.Expression] = (*Process)(nil)
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,15 +0,0 @@
// Package expr provides the abstract Expression interface for all evaluatable
// expression types in the lambda interpreter.
package expr
import (
"fmt"
)
// Expression is the base interface for all evaluatable expression types.
// Different evaluation modes (lambda calculus, SKI combinators, typed lambda
// calculus, etc.) implement this interface with their own concrete types.
type Expression interface {
// The expression should have a human-readable representation.
fmt.Stringer
}

View File

@@ -1,13 +1,32 @@
package lambda package lambda
import ( import (
"git.maximhutz.com/max/lambda/pkg/expr" "fmt"
"git.maximhutz.com/max/lambda/pkg/set"
) )
// Expression is the interface for all lambda calculus expression types. // Expression is the interface for all lambda calculus expression types.
// It embeds the general expr.Expression interface for cross-mode compatibility. // It embeds the general expr.Expression interface for cross-mode compatibility.
type Expression interface { type Expression interface {
expr.Expression fmt.Stringer
// Substitute replaces all free occurrences of the target variable with the
// replacement expression. Alpha-renaming is performed automatically to
// avoid variable capture.
Substitute(target string, replacement Expression) Expression
// GetFree returns the set of all free variable names in the expression.
// This function does not mutate the input expression.
// The returned set is newly allocated and can be modified by the caller.
GetFree() set.Set[string]
// Rename replaces all occurrences of the target variable name with the new name.
Rename(target string, newName string) Expression
// IsFree returns true if the variable name n occurs free in the expression.
// This function does not mutate the input expression.
IsFree(n string) bool
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
@@ -17,22 +36,22 @@ type Abstraction struct {
body Expression body Expression
} }
var _ Expression = (*Abstraction)(nil) var _ Expression = Abstraction{}
func (a *Abstraction) Parameter() string { 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) String() string { func (a Abstraction) String() string {
return "\\" + a.parameter + "." + a.body.String() return "\\" + a.parameter + "." + a.body.String()
} }
func NewAbstraction(parameter string, body Expression) *Abstraction { func NewAbstraction(parameter string, body Expression) Abstraction {
return &Abstraction{parameter, body} return Abstraction{parameter, body}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
@@ -42,40 +61,40 @@ type Application struct {
argument Expression argument Expression
} }
var _ Expression = (*Application)(nil) var _ Expression = Application{}
func (a *Application) Abstraction() Expression { 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) String() string { func (a Application) String() string {
return "(" + a.abstraction.String() + " " + a.argument.String() + ")" return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
} }
func NewApplication(abstraction Expression, argument Expression) *Application { func NewApplication(abstraction Expression, argument Expression) Application {
return &Application{abstraction, argument} return Application{abstraction, argument}
} }
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
type Variable struct { type Variable struct {
value string name string
} }
var _ Expression = (*Variable)(nil) var _ Expression = Variable{}
func (v *Variable) Value() string { func (v Variable) Name() string {
return v.value return v.name
} }
func (v *Variable) String() string { func (v Variable) String() string {
return v.value return v.name
} }
func NewVariable(name string) *Variable { func NewVariable(name string) Variable {
return &Variable{name} return Variable{name}
} }

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: func (e Abstraction) GetFree() set.Set[string] {
vars := GetFreeVariables(e.body) vars := e.Body().GetFree()
vars.Remove(e.parameter) vars.Remove(e.Parameter())
return vars return vars
case *Application: }
vars := GetFreeVariables(e.abstraction)
vars.Merge(GetFreeVariables(e.argument)) func (e Application) GetFree() set.Set[string] {
return vars vars := e.Abstraction().GetFree()
default: vars.Merge(e.Argument().GetFree())
return nil return vars
}
} }

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: func (e Abstraction) IsFree(n string) bool {
return e.parameter != n && IsFreeVariable(n, e.body) return e.Parameter() != n && e.Body().IsFree(n)
case *Application: }
return IsFreeVariable(n, e.abstraction) || IsFreeVariable(n, e.argument) func (e Application) IsFree(n string) bool {
default: return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
return false
}
} }

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,61 +0,0 @@
package lambda
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions.
type NormalOrderReducer struct {
emitter.BaseEmitter[reducer.Event]
expression *Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
return &NormalOrderReducer{
BaseEmitter: *emitter.New[reducer.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *NormalOrderReducer) Expression() expr.Expression {
return *r.expression
}
func isViable(e *Expression) (*Abstraction, Expression, bool) {
if e == nil {
return nil, nil, false
} else if app, appOk := (*e).(*Application); !appOk {
return nil, nil, false
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
return nil, nil, false
} else {
return fn, app.argument, true
}
}
// Reduce performs normal order reduction on a lambda expression.
// The expression must be a lambda.Expression; other types are returned unchanged.
func (r *NormalOrderReducer) Reduce() {
r.Emit(reducer.StartEvent)
it := NewIterator(r.expression)
for !it.Done() {
if fn, arg, ok := isViable(it.Current()); !ok {
it.Next()
} else {
it.Swap(Substitute(fn.body, fn.parameter, arg))
r.Emit(reducer.StepEvent)
if _, _, ok := isViable(it.Parent()); ok {
it.Back()
}
}
}
r.Emit(reducer.StopEvent)
}

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: func (e Application) Rename(target string, newName string) Expression {
newAbs := Rename(e.abstraction, target, newName) newAbs := e.Abstraction().Rename(target, newName)
newArg := Rename(e.argument, target, newName) newArg := e.Argument().Rename(target, newName)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg) return NewApplication(newAbs, newArg)
default:
return expr
}
} }

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: return e
if e.parameter == target { }
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
if e.Parameter() == target {
return e return e
} }
body := e.body body := e.Body()
param := e.parameter param := e.Parameter()
if IsFreeVariable(param, replacement) { if replacement.IsFree(param) {
freeVars := GetFreeVariables(replacement) freeVars := replacement.GetFree()
freeVars.Merge(GetFreeVariables(body)) freeVars.Merge(body.GetFree())
freshVar := GenerateFreshName(freeVars) freshVar := GenerateFreshName(freeVars)
body = Rename(body, param, freshVar) body = body.Rename(param, freshVar)
param = freshVar param = freshVar
} }
newBody := Substitute(body, target, replacement) newBody := body.Substitute(target, replacement)
if newBody == body && param == e.parameter {
return e
}
return NewAbstraction(param, newBody) return NewAbstraction(param, newBody)
}
case *Application:
newAbs := Substitute(e.abstraction, target, replacement) func (e Application) Substitute(target string, replacement Expression) Expression {
newArg := Substitute(e.argument, target, replacement) abs := e.Abstraction().Substitute(target, replacement)
arg := e.Argument().Substitute(target, replacement)
if newAbs == e.abstraction && newArg == e.argument {
return e return NewApplication(abs, arg)
}
return NewApplication(newAbs, newArg)
default:
return expr
}
} }

View File

@@ -1,13 +0,0 @@
package reducer
// Event represents lifecycle events during reduction.
type Event int
const (
// StartEvent is emitted before reduction begins.
StartEvent Event = iota
// StepEvent is emitted after each reduction step.
StepEvent
// StopEvent is emitted after reduction completes.
StopEvent
)

View File

@@ -1,27 +0,0 @@
// Package reducer provides the abstract Reducer interface for all expression
// reduction strategies.
package reducer
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
)
// Reducer defines the interface for expression reduction strategies.
// Different evaluation modes (normal order, applicative order, SKI combinators,
// etc.) implement this interface with their own reduction logic.
//
// Reducers also implement the Emitter interface to allow plugins to observe
// reduction lifecycle events (Start, Step, Stop).
type Reducer interface {
emitter.Emitter[Event]
// Reduce performs all reduction steps on the expression.
// Emits StartEvent before reduction, StepEvent after each step, and
// StopEvent after completion.
// Returns the final reduced expression.
Reduce()
// Expression returns the current expression state.
Expression() expr.Expression
}

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,27 +56,27 @@ 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 == '#': case letter == '#':
// Skip everything until the next newline or EOF. // Skip everything until the next newline or EOF.
for !i.Done() { for !i.Done() {
@@ -98,27 +98,27 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
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

@@ -4,9 +4,9 @@ import "iter"
type Set[T comparable] map[T]bool type Set[T comparable] map[T]bool
func (s *Set[T]) Add(items ...T) { func (s Set[T]) Add(items ...T) {
for _, item := range items { for _, item := range items {
(*s)[item] = true s[item] = true
} }
} }
@@ -14,14 +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)
} }
} }
@@ -46,8 +46,8 @@ func (s Set[T]) Items() iter.Seq[T] {
} }
} }
func New[T comparable](items ...T) *Set[T] { func New[T comparable](items ...T) Set[T] {
result := &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,8 +0,0 @@
0 := \f.\x.x
inc n := \f x.(f (n f x))
exp n m := (m n)
print n := (n F X)
N := (inc (inc (inc (inc (inc (inc 0))))))
(print (exp N N))