8 Commits

Author SHA1 Message Date
30e3bfe8f8 style: scanner not lexer 2026-01-19 19:40:55 -05:00
091bc70172 feat: copied code over 2026-01-19 19:32:08 -05:00
5f2dcc9394 feat: progress 2026-01-18 18:27:48 -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
70 changed files with 608 additions and 1954 deletions

View File

@@ -1,6 +1,6 @@
---
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: "
ref: "main"
assignees: []

View File

@@ -1,6 +1,6 @@
---
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: "
ref: "main"
assignees: []

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

View File

@@ -1,49 +1,30 @@
BINARY_NAME=lambda
TEST=simple
.PHONY: help build run profile explain graph docs test bench clean
.PHONY: help build docs test bench clean
.DEFAULT_GOAL := help
.SILENT:
help:
echo "Available targets:"
echo " build - Build the lambda executable"
echo " run - Build and run the lambda interpreter (use TEST=<name> to specify sample)"
echo " profile - Build and run with CPU profiling enabled"
echo " explain - Build and run with explanation mode and profiling"
echo " graph - Generate and open CPU profile visualization"
echo " docs - Start local godoc server on port 6060"
echo " test - Run tests for all samples"
echo " bench - Run benchmarks for all samples"
echo " test - Run tests"
echo " bench - Run benchmarks"
echo " clean - Remove all build artifacts"
build:
go build -o ${BINARY_NAME} ./cmd/lambda
chmod +x ${BINARY_NAME}
run: build
./${BINARY_NAME} -s -f ./tests/$(TEST).test -o program.out
profile: build
./${BINARY_NAME} -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out
explain: build
./${BINARY_NAME} -x -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out > explain.out
graph:
go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof
go tool pprof -svg profile/cpu.prof > profile/cpu.svg
echo ">>> View at 'file://$(PWD)/profile/cpu.svg'"
docs:
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
go run golang.org/x/tools/cmd/godoc@latest -http=:6060
test:
go test -v ./cmd/lambda
go test -v ./...
bench:
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
go test -bench=. -benchtime=10x -cpu=4 ./...
clean:
rm -f ${BINARY_NAME}

View File

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

30
cmd/lambda/convert.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var convertCmd = &cobra.Command{
Use: "convert [input] [output]",
Short: "Convert between lambda calculus representations",
Long: `Convert lambda calculus expressions between different representations.
The format is inferred from file extensions when possible.
Use --from and --to flags to override format detection.`,
Example: ` lambda convert input.sch output.dbi
lambda convert input.txt output.txt --from saccharine --to lambda`,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "not implemented")
os.Exit(1)
},
}
func init() {
convertCmd.Flags().String("from", "", "source format")
convertCmd.Flags().String("to", "", "target format")
rootCmd.AddCommand(convertCmd)
}

21
cmd/lambda/engine.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var engineCmd = &cobra.Command{
Use: "engine",
Short: "List available evaluation engines",
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "not implemented")
os.Exit(1)
},
}
func init() {
rootCmd.AddCommand(engineCmd)
}

View File

@@ -1,85 +0,0 @@
package main
import (
"os"
"git.maximhutz.com/max/lambda/internal/cli"
"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/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/reducer"
"git.maximhutz.com/max/lambda/pkg/saccharine"
)
func main() {
// Parse CLI arguments.
options, err := config.FromArgs()
cli.HandleError(err)
logger := options.GetLogger()
logger.Info("using program arguments", "args", os.Args)
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 based on the selected interpreter.
var red reducer.Reducer
switch options.Interpreter {
case config.DeBruijnInterpreter:
dbExpr := convert.LambdaToDeBruijn(compiled)
logger.Info("converted to De Bruijn", "tree", dbExpr.String())
red = debruijn.NewNormalOrderReducer(&dbExpr)
default:
red = lambda.NewNormalOrderReducer(&compiled)
}
// If the user selected to track CPU performance, attach a profiler.
if options.Profile != "" {
plugins.NewPerformance(options.Profile, red)
}
// If the user selected to produce a step-by-step explanation, attach an
// observer.
if options.Explanation {
plugins.NewExplanation(red)
}
// If the user opted to track statistics, attach a tracker.
if options.Statistics {
plugins.NewStatistics(red)
}
// If the user selected for verbose debug logs, attach a reduction tracker.
if options.Verbose {
plugins.NewLogs(logger, red)
}
// Run reduction.
red.Reduce()
// Return the final reduced result.
// For De Bruijn, convert back to lambda for consistent output.
var result string
if options.Interpreter == config.DeBruijnInterpreter {
dbExpr := red.Expression().(debruijn.Expression)
lambdaExpr := convert.DeBruijnToLambda(dbExpr)
result = lambdaExpr.String()
} else {
result = red.Expression().String()
}
err = options.Destination.Write(result)
cli.HandleError(err)
}

View File

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

27
cmd/lambda/repl.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var replCmd = &cobra.Command{
Use: "repl",
Short: "Start an interactive lambda calculus REPL",
Long: `Start an interactive Read-Eval-Print Loop for lambda calculus.
Enter lambda expressions to evaluate them.
Type 'exit' or 'quit' to leave the REPL.
Press Ctrl+D to exit.`,
Example: ` lambda repl`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "not implemented")
os.Exit(1)
},
}
func init() {
rootCmd.AddCommand(replCmd)
}

21
cmd/lambda/repr.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var reprCmd = &cobra.Command{
Use: "repr",
Short: "List available representations",
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "not implemented")
os.Exit(1)
},
}
func init() {
rootCmd.AddCommand(reprCmd)
}

19
cmd/lambda/root.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "lambda",
Short: "A lambda calculus interpreter and toolkit",
Long: `Lambda is a CLI tool for working with lambda calculus expressions.`,
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

31
cmd/lambda/run.go Normal file
View File

@@ -0,0 +1,31 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var runCmd = &cobra.Command{
Use: "run [expression]",
Short: "Evaluate a lambda calculus expression",
Long: `Evaluate a lambda calculus expression and print the result.
The expression can be provided as an argument, read from a file with --file,
or piped through stdin.`,
Example: ` lambda run "\x.x"
echo "\x.x" | lambda run
lambda run --file program.sch
lambda run --file program.sch --engine krivine`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "not implemented")
os.Exit(1)
},
}
func init() {
runCmd.Flags().StringP("file", "f", "", "read expression from file")
runCmd.Flags().StringP("engine", "e", "normal", "evaluation engine to use")
rootCmd.AddCommand(runCmd)
}

28
cmd/lambda/trace.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var traceCmd = &cobra.Command{
Use: "trace [file]",
Short: "Trace the evaluation of a lambda calculus expression",
Long: `Trace the step-by-step evaluation of a lambda calculus expression.
This command shows each reduction step during evaluation.`,
Example: ` lambda trace program.sch
lambda trace program.sch --engine krivine`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "not implemented")
os.Exit(1)
},
}
func init() {
traceCmd.Flags().StringP("engine", "e", "normal", "evaluation engine to use")
rootCmd.AddCommand(traceCmd)
}

3
go.mod
View File

@@ -6,6 +6,9 @@ require github.com/stretchr/testify v1.11.1
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/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

9
go.sum
View File

@@ -1,9 +1,18 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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

View File

@@ -1,21 +0,0 @@
// Package "config" parses ad handles the user settings given to the program.
package config
// Interpreter specifies the reduction engine to use.
type Interpreter string
const (
LambdaInterpreter Interpreter = "lambda"
DeBruijnInterpreter Interpreter = "debruijn"
)
// Configuration settings for the program.
type Config struct {
Source Source // The source code given to the program.
Destination Destination // The destination for the final result.
Verbose bool // Whether or not to print debug logs.
Explanation bool // Whether or not to print an explanation of the reduction.
Profile string // If not nil, print a CPU profile during execution.
Statistics bool // Whether or not to print statistics.
Interpreter Interpreter // The interpreter engine to use.
}

View File

@@ -1,27 +0,0 @@
package config
import (
"fmt"
"os"
)
// A method of writing output to the user.
type Destination interface {
// Write data to this destination.
Write(data string) error
}
// A destination writing to stdout.
type StdoutDestination struct{}
func (d StdoutDestination) Write(data string) error {
fmt.Println(data)
return nil
}
// A destination writing to a file.
type FileDestination struct{ Path string }
func (d FileDestination) Write(data string) error {
return os.WriteFile(d.Path, []byte(data+"\n"), 0644)
}

View File

@@ -1,23 +0,0 @@
package config
import (
"log/slog"
"os"
)
// Returns a structured logger with the appropriate configurations.
func (c Config) GetLogger() *slog.Logger {
// By default, only print out errors.
level := slog.LevelError
// If the user set the output to be "VERBOSE", return the debug logs.
if c.Verbose {
level = slog.LevelInfo
}
return slog.New(
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
}),
)
}

View File

@@ -1,69 +0,0 @@
package config
import (
"flag"
"fmt"
)
// Extract the program configuration from the command-line arguments.
func FromArgs() (*Config, error) {
// Relevant flags.
verbose := flag.Bool("v", false, "Verbosity. If set, the program will print logs.")
explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.")
statistics := flag.Bool("s", false, "Statistics. If set, the process will print various statistics about the run.")
profile := flag.String("p", "", "CPU profiling. If an output file is defined, the program will profile its execution and dump its results into it.")
file := flag.String("f", "", "File. If set, read source from the specified file.")
output := flag.String("o", "", "Output. If set, write result to the specified file. Use '-' for stdout (default).")
interpreter := flag.String("i", "lambda", "Interpreter. The reduction engine to use: 'lambda' or 'debruijn'.")
flag.Parse()
// Validate interpreter flag.
var interpType Interpreter
switch *interpreter {
case "lambda":
interpType = LambdaInterpreter
case "debruijn":
interpType = DeBruijnInterpreter
default:
return nil, fmt.Errorf("invalid interpreter: %s (must be 'lambda' or 'debruijn')", *interpreter)
}
// Parse source type.
var source Source
if *file != "" {
// File flag takes precedence.
if flag.NArg() > 0 {
return nil, fmt.Errorf("cannot specify both -f flag and positional argument")
}
source = FileSource{Path: *file}
} else if flag.NArg() == 0 {
return nil, fmt.Errorf("no input given")
} else if flag.NArg() > 1 {
return nil, fmt.Errorf("more than 1 command-line argument")
} else {
// Positional argument.
if flag.Arg(0) == "-" {
source = StdinSource{}
} else {
source = StringSource{Data: flag.Arg(0)}
}
}
// Parse destination type.
var destination Destination
if *output == "" || *output == "-" {
destination = StdoutDestination{}
} else {
destination = FileDestination{Path: *output}
}
return &Config{
Source: source,
Destination: destination,
Verbose: *verbose,
Explanation: *explanation,
Profile: *profile,
Statistics: *statistics,
Interpreter: interpType,
}, nil
}

View File

@@ -1,41 +0,0 @@
package config
import (
"io"
"os"
)
// A method of extracting input from the user.
type Source interface {
// Fetch data from this source.
Extract() (string, error)
}
// A source defined by a string.
type StringSource struct{ Data string }
func (s StringSource) Extract() (string, error) { return s.Data, nil }
// A source pulling from standard input.
type StdinSource struct{}
func (s StdinSource) Extract() (string, error) {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return "", err
}
return string(data), nil
}
// A source reading from a file.
type FileSource struct{ Path string }
func (s FileSource) Extract() (string, error) {
data, err := os.ReadFile(s.Path)
if err != nil {
return "", err
}
return string(data), nil
}

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

@@ -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,82 +0,0 @@
package convert
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/set"
)
// DeBruijnToLambda converts a De Bruijn indexed expression back to named lambda calculus.
func DeBruijnToLambda(expr debruijn.Expression) lambda.Expression {
return deBruijnToLambdaWithContext(expr, []string{})
}
func deBruijnToLambdaWithContext(expr debruijn.Expression, context []string) lambda.Expression {
switch e := expr.(type) {
case *debruijn.Variable:
index := e.Index()
if index < len(context) {
// Bound variable: look up name in context.
name := context[len(context)-1-index]
return lambda.NewVariable(name)
}
// Free variable: use the label if available.
if e.Label() != "" {
return lambda.NewVariable(e.Label())
}
// Generate a name for free variables without labels.
return lambda.NewVariable(fmt.Sprintf("free%d", index))
case *debruijn.Abstraction:
// Generate a fresh parameter name.
used := collectUsedNames(e.Body(), context)
paramName := generateFreshName(used)
newContext := append(context, paramName)
body := deBruijnToLambdaWithContext(e.Body(), newContext)
return lambda.NewAbstraction(paramName, body)
case *debruijn.Application:
abs := deBruijnToLambdaWithContext(e.Abstraction(), context)
arg := deBruijnToLambdaWithContext(e.Argument(), context)
return lambda.NewApplication(abs, arg)
default:
panic("unknown expression type")
}
}
// collectUsedNames gathers all variable labels used in an expression.
func collectUsedNames(expr debruijn.Expression, context []string) *set.Set[string] {
used := set.New[string]()
for _, name := range context {
used.Add(name)
}
collectUsedNamesHelper(expr, used)
return used
}
func collectUsedNamesHelper(expr debruijn.Expression, used *set.Set[string]) {
switch e := expr.(type) {
case *debruijn.Variable:
if e.Label() != "" {
used.Add(e.Label())
}
case *debruijn.Abstraction:
collectUsedNamesHelper(e.Body(), used)
case *debruijn.Application:
collectUsedNamesHelper(e.Abstraction(), used)
collectUsedNamesHelper(e.Argument(), used)
}
}
// generateFreshName creates a fresh variable name not in the used set.
func generateFreshName(used *set.Set[string]) string {
for i := 0; ; i++ {
name := fmt.Sprintf("_%d", i)
if !used.Has(name) {
return name
}
}
}

View File

@@ -1,44 +0,0 @@
package convert
import (
"git.maximhutz.com/max/lambda/pkg/debruijn"
"git.maximhutz.com/max/lambda/pkg/lambda"
)
// LambdaToDeBruijn converts a lambda calculus expression to De Bruijn indexed form.
// The context parameter tracks bound variables from outer abstractions.
func LambdaToDeBruijn(expr lambda.Expression) debruijn.Expression {
return lambdaToDeBruijnWithContext(expr, []string{})
}
func lambdaToDeBruijnWithContext(expr lambda.Expression, context []string) debruijn.Expression {
switch e := expr.(type) {
case *lambda.Variable:
name := e.Value()
// Search for the variable in the context (innermost to outermost).
for i := len(context) - 1; i >= 0; i-- {
if context[i] == name {
index := len(context) - 1 - i
return debruijn.NewVariable(index, name)
}
}
// Free variable: use a negative index to mark it.
// We encode free variables with index = len(context) + position.
// For simplicity, we use a large index that won't conflict.
return debruijn.NewVariable(len(context), name)
case *lambda.Abstraction:
// Add the parameter to the context.
newContext := append(context, e.Parameter())
body := lambdaToDeBruijnWithContext(e.Body(), newContext)
return debruijn.NewAbstraction(body)
case *lambda.Application:
abs := lambdaToDeBruijnWithContext(e.Abstraction(), context)
arg := lambdaToDeBruijnWithContext(e.Argument(), context)
return debruijn.NewApplication(abs, arg)
default:
panic("unknown expression type")
}
}

View File

@@ -3,11 +3,11 @@ package convert
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
"git.maximhutz.com/max/lambda/pkg/repr/lambda"
"git.maximhutz.com/max/lambda/pkg/repr/saccharine"
)
func convertAtom(n *saccharine.Atom) lambda.Expression {
func convertAtom(n *saccharine.Variable) lambda.Expression {
return lambda.NewVariable(n.Name)
}
@@ -19,7 +19,7 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
// If the function has no parameters, it is a thunk. Lambda calculus still
// requires _some_ parameter exists, so generate one.
if len(parameters) == 0 {
freeVars := lambda.GetFreeVariables(result)
freeVars := result.GetFree()
freshName := lambda.GenerateFreshName(freeVars)
parameters = append(parameters, freshName)
}
@@ -63,7 +63,7 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
}
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
freshVar := lambda.GenerateFreshName(e.GetFree())
return lambda.NewApplication(
lambda.NewAbstraction(freshVar, e),
@@ -94,7 +94,7 @@ func convertClause(n *saccharine.Clause) lambda.Expression {
func SaccharineToLambda(n saccharine.Expression) lambda.Expression {
switch n := n.(type) {
case *saccharine.Atom:
case *saccharine.Variable:
return convertAtom(n)
case *saccharine.Abstraction:
return convertAbstraction(n)

View File

@@ -1,119 +0,0 @@
// Package debruijn provides De Bruijn indexed lambda calculus expressions.
// De Bruijn indices eliminate the need for variable names by using numeric
// indices to refer to bound variables, avoiding capture issues during substitution.
package debruijn
import "git.maximhutz.com/max/lambda/pkg/expr"
// Expression is the interface for all De Bruijn indexed expression types.
// It embeds the general expr.Expression interface for cross-mode compatibility.
type Expression interface {
expr.Expression
Accept(Visitor)
}
/** ------------------------------------------------------------------------- */
// Abstraction represents a lambda abstraction without a named parameter.
// In De Bruijn notation, the parameter is implicit and referenced by index 0
// within the body.
type Abstraction struct {
body Expression
}
// Body returns the body of the abstraction.
func (a *Abstraction) Body() Expression {
return a.body
}
// Accept implements the Visitor pattern.
func (a *Abstraction) Accept(v Visitor) {
v.VisitAbstraction(a)
}
// String returns the De Bruijn notation string representation.
func (a *Abstraction) String() string {
return Stringify(a)
}
// NewAbstraction creates a new De Bruijn abstraction with the given body.
func NewAbstraction(body Expression) *Abstraction {
return &Abstraction{body: body}
}
/** ------------------------------------------------------------------------- */
// Application represents the application of one expression to another.
type Application struct {
abstraction Expression
argument Expression
}
// Abstraction returns the function expression being applied.
func (a *Application) Abstraction() Expression {
return a.abstraction
}
// Argument returns the argument expression.
func (a *Application) Argument() Expression {
return a.argument
}
// Accept implements the Visitor pattern.
func (a *Application) Accept(v Visitor) {
v.VisitApplication(a)
}
// String returns the De Bruijn notation string representation.
func (a *Application) String() string {
return Stringify(a)
}
// NewApplication creates a new application expression.
func NewApplication(abstraction Expression, argument Expression) *Application {
return &Application{abstraction: abstraction, argument: argument}
}
/** ------------------------------------------------------------------------- */
// Variable represents a De Bruijn indexed variable.
// The index indicates how many binders to skip to find the binding abstraction.
// The label is an optional hint for display purposes.
type Variable struct {
index int
label string
}
// Index returns the De Bruijn index.
func (v *Variable) Index() int {
return v.index
}
// Label returns the optional variable label.
func (v *Variable) Label() string {
return v.label
}
// Accept implements the Visitor pattern.
func (v *Variable) Accept(visitor Visitor) {
visitor.VisitVariable(v)
}
// String returns the De Bruijn notation string representation.
func (v *Variable) String() string {
return Stringify(v)
}
// NewVariable creates a new De Bruijn variable with the given index and label.
func NewVariable(index int, label string) *Variable {
return &Variable{index: index, label: label}
}
/** ------------------------------------------------------------------------- */
// Visitor interface for traversing De Bruijn expressions.
type Visitor interface {
VisitAbstraction(*Abstraction)
VisitApplication(*Application)
VisitVariable(*Variable)
}

View File

@@ -1,76 +0,0 @@
package debruijn
// Iterator provides depth-first traversal of De Bruijn expressions.
type Iterator struct {
trace []*Expression
}
// NewIterator creates a new iterator starting at the given expression.
func NewIterator(expr *Expression) *Iterator {
return &Iterator{[]*Expression{expr}}
}
// Done returns true when the iterator has finished traversal.
func (i *Iterator) Done() bool {
return len(i.trace) == 0
}
// Current returns a pointer to the current expression.
func (i *Iterator) Current() *Expression {
if i.Done() {
return nil
}
return i.trace[len(i.trace)-1]
}
// Parent returns a pointer to the parent expression.
func (i *Iterator) Parent() *Expression {
if len(i.trace) < 2 {
return nil
}
return i.trace[len(i.trace)-2]
}
// Swap replaces the current expression with the given expression.
func (i *Iterator) Swap(with Expression) {
current := i.Current()
if current != nil {
*current = with
}
}
// Back moves the iterator back to the parent expression.
func (i *Iterator) Back() bool {
if i.Done() {
return false
}
i.trace = i.trace[:len(i.trace)-1]
return true
}
// Next advances the iterator to the next expression in leftmost-outermost order.
func (i *Iterator) Next() {
switch typed := (*i.Current()).(type) {
case *Abstraction:
i.trace = append(i.trace, &typed.body)
case *Application:
i.trace = append(i.trace, &typed.abstraction)
case *Variable:
for len(i.trace) > 1 {
if app, ok := (*i.Parent()).(*Application); ok {
if app.abstraction == *i.Current() {
i.Back()
i.trace = append(i.trace, &app.argument)
return
}
}
i.Back()
}
i.trace = []*Expression{}
}
}

View File

@@ -1,72 +0,0 @@
package debruijn
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/reducer"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for De Bruijn indexed lambda calculus expressions.
type NormalOrderReducer struct {
emitter.BaseEmitter[reducer.Event]
expression *Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewNormalOrderReducer(expression *Expression) *NormalOrderReducer {
return &NormalOrderReducer{
BaseEmitter: *emitter.New[reducer.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *NormalOrderReducer) Expression() expr.Expression {
return *r.expression
}
// isViable checks if an expression is a redex (reducible expression).
// A redex is an application of an abstraction to an argument.
func isViable(e *Expression) (*Abstraction, Expression, bool) {
if e == nil {
return nil, nil, false
} else if app, appOk := (*e).(*Application); !appOk {
return nil, nil, false
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
return nil, nil, false
} else {
return fn, app.argument, true
}
}
// betaReduce performs a single beta reduction step.
// Given (\. body) arg, it substitutes arg for index 0 in body,
// then shifts the result down to account for the removed abstraction.
func betaReduce(fn *Abstraction, arg Expression) Expression {
// Substitute arg for variable 0 in the body.
substituted := Substitute(fn.body, 0, Shift(arg, 1, 0))
// Shift down to account for the removed abstraction.
return Shift(substituted, -1, 0)
}
// Reduce performs normal order reduction on a De Bruijn expression.
func (r *NormalOrderReducer) Reduce() {
r.Emit(reducer.StartEvent)
it := NewIterator(r.expression)
for !it.Done() {
if fn, arg, ok := isViable(it.Current()); !ok {
it.Next()
} else {
it.Swap(betaReduce(fn, arg))
r.Emit(reducer.StepEvent)
if _, _, ok := isViable(it.Parent()); ok {
it.Back()
}
}
}
r.Emit(reducer.StopEvent)
}

View File

@@ -1,32 +0,0 @@
package debruijn
// Shift increments all free variable indices in an expression by the given amount.
// A variable is free if its index is >= the cutoff (depth of nested abstractions).
// This is necessary when substituting an expression into a different binding context.
func Shift(expr Expression, amount int, cutoff int) Expression {
switch e := expr.(type) {
case *Variable:
if e.index >= cutoff {
return NewVariable(e.index+amount, e.label)
}
return e
case *Abstraction:
newBody := Shift(e.body, amount, cutoff+1)
if newBody == e.body {
return e
}
return NewAbstraction(newBody)
case *Application:
newAbs := Shift(e.abstraction, amount, cutoff)
newArg := Shift(e.argument, amount, cutoff)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg)
default:
return expr
}
}

View File

@@ -1,35 +0,0 @@
package debruijn
import (
"strconv"
"strings"
)
type stringifyVisitor struct {
builder strings.Builder
}
func (v *stringifyVisitor) VisitVariable(a *Variable) {
v.builder.WriteString(strconv.Itoa(a.index))
}
func (v *stringifyVisitor) VisitAbstraction(f *Abstraction) {
v.builder.WriteRune('\\')
v.builder.WriteRune('.')
f.body.Accept(v)
}
func (v *stringifyVisitor) VisitApplication(c *Application) {
v.builder.WriteRune('(')
c.abstraction.Accept(v)
v.builder.WriteRune(' ')
c.argument.Accept(v)
v.builder.WriteRune(')')
}
// Stringify converts a De Bruijn expression to its string representation.
func Stringify(e Expression) string {
b := &stringifyVisitor{builder: strings.Builder{}}
e.Accept(b)
return b.builder.String()
}

View File

@@ -1,34 +0,0 @@
package debruijn
// Substitute replaces the variable at the given index with the replacement expression.
// The replacement is shifted appropriately as we descend into nested abstractions.
func Substitute(expr Expression, index int, replacement Expression) Expression {
switch e := expr.(type) {
case *Variable:
if e.index == index {
return replacement
}
return e
case *Abstraction:
// When entering an abstraction, increment the target index and shift the
// replacement to account for the new binding context.
shiftedReplacement := Shift(replacement, 1, 0)
newBody := Substitute(e.body, index+1, shiftedReplacement)
if newBody == e.body {
return e
}
return NewAbstraction(newBody)
case *Application:
newAbs := Substitute(e.abstraction, index, replacement)
newArg := Substitute(e.argument, index, replacement)
if newAbs == e.abstraction && newArg == e.argument {
return e
}
return NewApplication(newAbs, newArg)
default:
return expr
}
}

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

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

View File

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

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

@@ -0,0 +1,23 @@
package engine
import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/repr"
)
type Engine[R repr.Repr] interface {
Run(trace bool, count int) (Trace, bool)
Step() uint64
State() (repr.Repr, error)
fmt.Stringer
}
type Trace interface {
Before() string
After() string
Step() uint64
TransitionName() string
TransitionNodes() string
}

View File

@@ -1,11 +0,0 @@
// Package expr provides the abstract Expression interface for all evaluatable
// expression types in the lambda interpreter.
package expr
// Expression is the base interface for all evaluatable expression types.
// Different evaluation modes (lambda calculus, SKI combinators, typed lambda
// calculus, etc.) implement this interface with their own concrete types.
type Expression interface {
// String returns a human-readable representation of the expression.
String() string
}

View File

@@ -1,94 +0,0 @@
package lambda
import "git.maximhutz.com/max/lambda/pkg/expr"
// Expression is the interface for all lambda calculus expression types.
// It embeds the general expr.Expression interface for cross-mode compatibility.
type Expression interface {
expr.Expression
Accept(Visitor)
}
/** ------------------------------------------------------------------------- */
type Abstraction struct {
parameter string
body Expression
}
func (a *Abstraction) Parameter() string {
return a.parameter
}
func (a *Abstraction) Body() Expression {
return a.body
}
func (a *Abstraction) Accept(v Visitor) {
v.VisitAbstraction(a)
}
func (a *Abstraction) String() string {
return Stringify(a)
}
func NewAbstraction(parameter string, body Expression) *Abstraction {
return &Abstraction{parameter: parameter, body: body}
}
/** ------------------------------------------------------------------------- */
type Application struct {
abstraction Expression
argument Expression
}
func (a *Application) Abstraction() Expression {
return a.abstraction
}
func (a *Application) Argument() Expression {
return a.argument
}
func (a *Application) Accept(v Visitor) {
v.VisitApplication(a)
}
func (a *Application) String() string {
return Stringify(a)
}
func NewApplication(abstraction Expression, argument Expression) *Application {
return &Application{abstraction: abstraction, argument: argument}
}
/** ------------------------------------------------------------------------- */
type Variable struct {
value string
}
func (v *Variable) Value() string {
return v.value
}
func (v *Variable) Accept(visitor Visitor) {
visitor.VisitVariable(v)
}
func (v *Variable) String() string {
return Stringify(v)
}
func NewVariable(name string) *Variable {
return &Variable{value: name}
}
/** ------------------------------------------------------------------------- */
type Visitor interface {
VisitAbstraction(*Abstraction)
VisitApplication(*Application)
VisitVariable(*Variable)
}

View File

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

View File

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

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{}
}
}

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 +0,0 @@
package lambda
func Rename(expr Expression, target string, newName string) Expression {
switch e := expr.(type) {
case *Variable:
if e.value == target {
return NewVariable(newName)
}
return e
case *Abstraction:
newParam := e.parameter
if e.parameter == target {
newParam = newName
}
newBody := Rename(e.body, target, newName)
if newParam == e.parameter && newBody == e.body {
return e
}
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
}
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 +0,0 @@
package lambda
func Substitute(expr Expression, target string, replacement Expression) Expression {
switch e := expr.(type) {
case *Variable:
if e.value == target {
return replacement
}
return e
case *Abstraction:
if e.parameter == target {
return e
}
body := e.body
param := e.parameter
if IsFreeVariable(param, replacement) {
freeVars := GetFreeVariables(replacement)
freeVars.Merge(GetFreeVariables(body))
freshVar := GenerateFreshName(freeVars)
body = Rename(body, param, freshVar)
param = freshVar
}
newBody := Substitute(body, target, replacement)
if newBody == body && param == e.parameter {
return e
}
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)
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,87 @@
package lambda
import (
"git.maximhutz.com/max/lambda/pkg/repr"
"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 {
repr.Repr
// 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
}
var (
_ Expression = Abstraction{}
_ Expression = Application{}
_ Expression = Variable{}
)
/** ------------------------------------------------------------------------- */
type Abstraction struct {
parameter string
body Expression
}
func (a Abstraction) Parameter() string {
return a.parameter
}
func (a Abstraction) Body() Expression {
return a.body
}
func NewAbstraction(parameter string, body Expression) Abstraction {
return Abstraction{parameter, body}
}
/** ------------------------------------------------------------------------- */
type Application struct {
abstraction Expression
argument Expression
}
func (a Application) Abstraction() Expression {
return a.abstraction
}
func (a Application) Argument() Expression {
return a.argument
}
func NewApplication(abstraction Expression, argument Expression) Application {
return Application{abstraction, argument}
}
/** ------------------------------------------------------------------------- */
type Variable struct {
name string
}
func (v Variable) Name() string {
return v.name
}
func NewVariable(name string) Variable {
return Variable{name}
}

View File

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

View File

@@ -0,0 +1,19 @@
package lambda
import "git.maximhutz.com/max/lambda/pkg/set"
func (e Variable) GetFree() set.Set[string] {
return set.New(e.Name())
}
func (e Abstraction) GetFree() set.Set[string] {
vars := e.Body().GetFree()
vars.Remove(e.Parameter())
return vars
}
func (e Application) GetFree() set.Set[string] {
vars := e.Abstraction().GetFree()
vars.Merge(e.Argument().GetFree())
return vars
}

View File

@@ -0,0 +1,12 @@
package lambda
func (e Variable) IsFree(n string) bool {
return e.Name() == n
}
func (e Abstraction) IsFree(n string) bool {
return e.Parameter() != n && e.Body().IsFree(n)
}
func (e Application) IsFree(n string) bool {
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
}

28
pkg/repr/lambda/rename.go Normal file
View File

@@ -0,0 +1,28 @@
package lambda
// Rename replaces all occurrences of the target variable name with the new name.
func (e Variable) Rename(target string, newName string) Expression {
if e.Name() == target {
return NewVariable(newName)
}
return e
}
func (e Abstraction) Rename(target string, newName string) Expression {
newParam := e.Parameter()
if e.Parameter() == target {
newParam = newName
}
newBody := e.Body().Rename(target, newName)
return NewAbstraction(newParam, newBody)
}
func (e Application) Rename(target string, newName string) Expression {
newAbs := e.Abstraction().Rename(target, newName)
newArg := e.Argument().Rename(target, newName)
return NewApplication(newAbs, newArg)
}

View File

@@ -0,0 +1,35 @@
package lambda
func (e Variable) Substitute(target string, replacement Expression) Expression {
if e.Name() == target {
return replacement
}
return e
}
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
if e.Parameter() == target {
return e
}
body := e.Body()
param := e.Parameter()
if replacement.IsFree(param) {
freeVars := replacement.GetFree()
freeVars.Merge(body.GetFree())
freshVar := GenerateFreshName(freeVars)
body = body.Rename(param, freshVar)
param = freshVar
}
newBody := body.Substitute(target, replacement)
return NewAbstraction(param, newBody)
}
func (e Application) Substitute(target string, replacement Expression) Expression {
abs := e.Abstraction().Substitute(target, replacement)
arg := e.Argument().Substitute(target, replacement)
return NewApplication(abs, arg)
}

4
pkg/repr/repr.go Normal file
View File

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

View File

@@ -1,9 +1,20 @@
package saccharine
import (
"git.maximhutz.com/max/lambda/pkg/repr"
)
type Expression interface {
IsExpression()
repr.Repr
}
var (
_ Expression = Abstraction{}
_ Expression = Application{}
_ Expression = Variable{}
_ Expression = Clause{}
)
/** ------------------------------------------------------------------------- */
type Abstraction struct {
@@ -11,37 +22,36 @@ type Abstraction struct {
Body Expression
}
type Application struct {
Abstraction Expression
Arguments []Expression
func NewAbstraction(parameter []string, body Expression) *Abstraction {
return &Abstraction{Parameters: parameter, Body: body}
}
type Atom struct {
Name string
}
type Clause struct {
Statements []Statement
Returns Expression
}
func (Abstraction) IsExpression() {}
func (Application) IsExpression() {}
func (Atom) IsExpression() {}
func (Clause) IsExpression() {}
/** ------------------------------------------------------------------------- */
func NewAbstraction(parameter []string, body Expression) *Abstraction {
return &Abstraction{Parameters: parameter, Body: body}
type Application struct {
Abstraction Expression
Arguments []Expression
}
func NewApplication(abstraction Expression, arguments []Expression) *Application {
return &Application{Abstraction: abstraction, Arguments: arguments}
}
func NewAtom(name string) *Atom {
return &Atom{Name: name}
/** ------------------------------------------------------------------------- */
type Variable struct {
Name string
}
func NewVariable(name string) *Variable {
return &Variable{Name: name}
}
/** ------------------------------------------------------------------------- */
type Clause struct {
Statements []Statement
Returns Expression
}
func NewClause(statements []Statement, returns Expression) *Clause {

View File

@@ -5,18 +5,17 @@ import (
"fmt"
"git.maximhutz.com/max/lambda/pkg/iterator"
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
"git.maximhutz.com/max/lambda/pkg/trace"
)
type TokenIterator = iterator.Iterator[token.Token]
type TokenIterator = iterator.Iterator[Token]
func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
func parseRawToken(i *TokenIterator, expected Type) (*Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
if tok, err := i.Next(); err != nil {
return nil, err
} else if tok.Type != expected {
return nil, fmt.Errorf("expected token %v, got %v'", token.Name(expected), tok.Value)
return nil, fmt.Errorf("expected token %v, got %v'", Name(expected), tok.Value)
} else {
return &tok, nil
}
@@ -25,14 +24,14 @@ func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error)
func passSoftBreaks(i *TokenIterator) {
for {
if _, err := parseRawToken(i, token.SoftBreak); err != nil {
if _, err := parseRawToken(i, TokenSoftBreak); err != nil {
return
}
}
}
func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*token.Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
func parseToken(i *TokenIterator, expected Type, ignoreSoftBreaks bool) (*Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
if ignoreSoftBreaks {
passSoftBreaks(i)
}
@@ -42,17 +41,17 @@ func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*
}
func parseString(i *TokenIterator) (string, error) {
if tok, err := parseToken(i, token.Atom, true); err != nil {
if tok, err := parseToken(i, TokenAtom, true); err != nil {
return "", trace.Wrap(err, "no variable (col %d)", i.Index())
} else {
return tok.Value, nil
}
}
func parseBreak(i *TokenIterator) (*token.Token, error) {
if tok, softErr := parseRawToken(i, token.SoftBreak); softErr == nil {
func parseBreak(i *TokenIterator) (*Token, error) {
if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == nil {
return tok, nil
} else if tok, hardErr := parseRawToken(i, token.HardBreak); hardErr == nil {
} else if tok, hardErr := parseRawToken(i, TokenHardBreak); hardErr == nil {
return tok, nil
} else {
return nil, errors.Join(softErr, hardErr)
@@ -76,11 +75,11 @@ func parseList[U any](i *TokenIterator, fn func(*TokenIterator) (U, error), mini
func parseAbstraction(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)
} else if parameters, err := parseList(i, parseString, 0); err != nil {
return nil, err
} else if _, err = parseToken(i, token.Dot, true); err != nil {
} else if _, err = parseToken(i, TokenDot, true); err != nil {
return nil, trace.Wrap(err, "no function dot (col %d)", i.MustGet().Column)
} else if body, err := parseExpression(i); err != nil {
return nil, err
@@ -92,11 +91,11 @@ func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
func parseApplication(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)
} else if expressions, err := parseList(i, parseExpression, 1); err != nil {
return nil, err
} else if _, err := parseToken(i, token.CloseParen, true); err != nil {
} else if _, err := parseToken(i, TokenCloseParen, true); err != nil {
return nil, trace.Wrap(err, "no closing brackets (col %d)", i.MustGet().Column)
} else {
return NewApplication(expressions[0], expressions[1:]), nil
@@ -104,11 +103,11 @@ func parseApplication(i *TokenIterator) (*Application, error) {
})
}
func parseAtom(i *TokenIterator) (*Atom, error) {
if tok, err := parseToken(i, token.Atom, true); err != nil {
func parseAtom(i *TokenIterator) (*Variable, error) {
if tok, err := parseToken(i, TokenAtom, true); err != nil {
return nil, trace.Wrap(err, "no variable (col %d)", i.Index())
} else {
return NewAtom(tok.Value), nil
return NewVariable(tok.Value), nil
}
}
@@ -133,7 +132,7 @@ func parseStatements(i *TokenIterator) ([]Statement, error) {
func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
if braces {
if _, err := parseToken(i, token.OpenBrace, true); err != nil {
if _, err := parseToken(i, TokenOpenBrace, true); err != nil {
return nil, err
}
}
@@ -152,7 +151,7 @@ func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
}
if braces {
if _, err := parseToken(i, token.CloseBrace, true); err != nil {
if _, err := parseToken(i, TokenCloseBrace, true); err != nil {
return nil, err
}
}
@@ -165,13 +164,13 @@ func parseExpression(i *TokenIterator) (Expression, error) {
passSoftBreaks(i)
switch peek := i.MustGet(); peek.Type {
case token.OpenParen:
case TokenOpenParen:
return parseApplication(i)
case token.Slash:
case TokenSlash:
return parseAbstraction(i)
case token.Atom:
case TokenAtom:
return parseAtom(i)
case token.OpenBrace:
case TokenOpenBrace:
return parseClause(i, true)
default:
return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column)
@@ -183,7 +182,7 @@ func parseLet(i *TokenIterator) (*LetStatement, error) {
return iterator.Do(i, func(i *TokenIterator) (*LetStatement, error) {
if parameters, err := parseList(i, parseString, 1); err != nil {
return nil, err
} else if _, err := parseToken(i, token.Assign, true); err != nil {
} else if _, err := parseToken(i, TokenAssign, true); err != nil {
return nil, err
} else if body, err := parseExpression(i); err != nil {
return nil, err
@@ -212,7 +211,7 @@ func parseStatement(i *TokenIterator) (Statement, error) {
}
// 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)
exp, err := parseClause(i, false)
@@ -226,3 +225,13 @@ func parse(tokens []token.Token) (Expression, error) {
return exp, nil
}
// Convert a piece of valid saccharine code into an expression.
func Parse(code string) (Expression, error) {
tokens, err := Scan(code)
if err != nil {
return nil, err
}
return parse(tokens)
}

View File

@@ -1,4 +1,4 @@
package token
package saccharine
import (
"errors"
@@ -14,7 +14,7 @@ func isVariable(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r)
}
func parseRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
i2 := i.Copy()
if r, err := i2.Next(); err != nil {
@@ -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()
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
// 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()
if i.Done() {
@@ -70,7 +70,7 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
case letter == '}':
return NewCloseBrace(index), nil
case letter == ':':
if _, err := parseCharacter(i, '='); err != nil {
if _, err := scanCharacter(i, '='); err != nil {
return nil, err
} else {
return NewAssign(index), nil
@@ -98,7 +98,7 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
atom := []rune{letter}
for {
if r, err := parseRune(i, isVariable); err != nil {
if r, err := scanRune(i, isVariable); err != nil {
break
} else {
atom = append(atom, r)
@@ -112,13 +112,13 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
}
// Parse a string into tokens.
func Parse(input string) ([]Token, error) {
func Scan(input string) ([]Token, error) {
i := iterator.Of([]rune(input))
tokens := []Token{}
errorList := []error{}
for !i.Done() {
token, err := getToken(i)
token, err := scanToken(i)
if err != nil {
errorList = append(errorList, err)
} else if token != nil {

View File

@@ -1,7 +1,11 @@
package saccharine
import (
"git.maximhutz.com/max/lambda/pkg/repr"
)
type Statement interface {
IsStatement()
repr.Repr
}
/** ------------------------------------------------------------------------- */
@@ -12,17 +16,14 @@ type LetStatement struct {
Body Expression
}
type DeclareStatement struct {
Value Expression
func NewLet(name string, parameters []string, body Expression) *LetStatement {
return &LetStatement{Name: name, Parameters: parameters, Body: body}
}
func (LetStatement) IsStatement() {}
func (DeclareStatement) IsStatement() {}
/** ------------------------------------------------------------------------- */
func NewLet(name string, parameters []string, body Expression) *LetStatement {
return &LetStatement{Name: name, Parameters: parameters, Body: body}
type DeclareStatement struct {
Value Expression
}
func NewDeclare(value Expression) *DeclareStatement {

View File

@@ -5,30 +5,30 @@ import (
"strings"
)
func stringifyAtom(n *Atom) string {
func stringifyAtom(n *Variable) string {
return n.Name
}
func stringifyAbstraction(n *Abstraction) string {
return "\\" + strings.Join(n.Parameters, " ") + "." + stringifyExpression(n.Body)
return "\\" + strings.Join(n.Parameters, " ") + "." + Stringify(n.Body)
}
func stringifyApplication(n *Application) string {
arguments := []string{stringifyExpression(n.Abstraction)}
arguments := []string{Stringify(n.Abstraction)}
for _, argument := range n.Arguments {
arguments = append(arguments, stringifyExpression(argument))
arguments = append(arguments, Stringify(argument))
}
return "(" + strings.Join(arguments, " ") + ")"
}
func stringifyLet(s *LetStatement) string {
return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + stringifyExpression(s.Body)
return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + Stringify(s.Body)
}
func stringifyDeclare(s *DeclareStatement) string {
return stringifyExpression(s.Value)
return Stringify(s.Value)
}
func stringifyStatement(s Statement) string {
@@ -49,13 +49,13 @@ func stringifyClause(n *Clause) string {
stmts += stringifyStatement(statement) + "; "
}
return "{ " + stmts + stringifyExpression(n.Returns) + " }"
return "{ " + stmts + Stringify(n.Returns) + " }"
}
// Convert an expression back into valid source code.
func stringifyExpression(n Expression) string {
func Stringify(n Expression) string {
switch n := n.(type) {
case *Atom:
case *Variable:
return stringifyAtom(n)
case *Abstraction:
return stringifyAbstraction(n)

View File

@@ -0,0 +1,91 @@
package saccharine
import "fmt"
// All tokens in the pseudo-lambda language.
type Type int
const (
TokenOpenParen Type = 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 Type // What type the token is.
Value string // The value of the token.
}
func NewOpenParen(column int) *Token {
return &Token{Type: TokenOpenParen, Column: column, Value: "("}
}
func NewCloseParen(column int) *Token {
return &Token{Type: TokenCloseParen, Column: column, Value: ")"}
}
func NewOpenBrace(column int) *Token {
return &Token{Type: TokenOpenBrace, Column: column, Value: "{"}
}
func NewCloseBrace(column int) *Token {
return &Token{Type: TokenCloseBrace, Column: column, Value: "}"}
}
func NewDot(column int) *Token {
return &Token{Type: TokenDot, Column: column, Value: "."}
}
func NewHardBreak(column int) *Token {
return &Token{Type: TokenHardBreak, Column: column, Value: ";"}
}
func NewAssign(column int) *Token {
return &Token{Type: TokenAssign, Column: column, Value: ":="}
}
func NewSlash(column int) *Token {
return &Token{Type: TokenSlash, Column: column, Value: "\\"}
}
func NewAtom(name string, column int) *Token {
return &Token{Type: TokenAtom, Column: column, Value: name}
}
func NewSoftBreak(column int) *Token {
return &Token{Type: TokenSoftBreak, Column: column, Value: "\\n"}
}
func Name(typ Type) string {
switch typ {
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", typ))
}
}
func (t Token) Name() string {
return Name(t.Type)
}

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,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
func (s *Set[T]) Add(items ...T) {
func (s Set[T]) Add(items ...T) {
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]
}
func (s *Set[T]) Remove(items ...T) {
func (s Set[T]) Remove(items ...T) {
for _, item := range items {
delete(*s, item)
delete(s, item)
}
}
func (s *Set[T]) Merge(o *Set[T]) {
for item := range *o {
func (s Set[T]) Merge(o Set[T]) {
for item := range o {
s.Add(item)
}
}
@@ -46,8 +46,8 @@ func (s Set[T]) Items() iter.Seq[T] {
}
}
func New[T comparable](items ...T) *Set[T] {
result := &Set[T]{}
func New[T comparable](items ...T) Set[T] {
result := Set[T]{}
for _, item := range items {
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))