Compare commits
14 Commits
f3b9137d75
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
31924237b2
|
|||
|
68cc1624c7
|
|||
|
0cdce0e42c
|
|||
|
0ec52008bb
|
|||
| f2c8d9f7d2 | |||
| 9c7fb8ceba | |||
| e85cf7ceff | |||
| c2aa77cb92 | |||
| 52d40adcc6 | |||
| 1974ad582f | |||
| f8e1223463 | |||
| e0114c736d | |||
| 5c54f4e195 | |||
| 307b7ffd1e |
@@ -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: []
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -48,7 +48,7 @@ linters:
|
||||
# More information: https://golangci-lint.run/usage/false-positives/#comments
|
||||
#
|
||||
# Please uncomment the following line if your code is not using the godoc format
|
||||
- comments
|
||||
# - comments
|
||||
|
||||
# Common false positives
|
||||
# feel free to remove this if you don't have any false positives
|
||||
@@ -126,6 +126,9 @@ linters:
|
||||
# Blank import should be only in a main or test package, or have a comment justifying it.
|
||||
- name: blank-imports
|
||||
|
||||
# Packages should have comments of the form "Package x ...".
|
||||
- name: package-comments
|
||||
|
||||
# context.Context() should be the first parameter of a function when provided as argument.
|
||||
- name: context-as-argument
|
||||
arguments:
|
||||
|
||||
23
CLAUDE.md
23
CLAUDE.md
@@ -80,3 +80,26 @@ Use the `tea` CLI (Gitea command-line tool) for PR operations instead of `gh`.
|
||||
|
||||
**Linking issues**: When a PR solves an issue, reference the issue in both the commit message and PR description using `Closes #<number>`.
|
||||
This automatically links and closes the issue when the PR is merged.
|
||||
|
||||
### Updating PRs
|
||||
|
||||
When pushing additional changes to an existing PR, add a comment summarizing the new commits.
|
||||
This keeps reviewers informed of what changed since the initial PR description.
|
||||
|
||||
Use the `tea` CLI to add comments to pull requests:
|
||||
|
||||
```bash
|
||||
tea comment <number> "Comment text"
|
||||
```
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Add a comment to PR #42
|
||||
tea comment 42 "Updated implementation based on feedback"
|
||||
|
||||
# Add a multi-line comment
|
||||
tea comment 42 "Summary of changes:
|
||||
- Fixed bug in reducer
|
||||
- Added new tests"
|
||||
```
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -48,7 +48,7 @@ The "source code" for a work means the preferred form of the work for making mod
|
||||
|
||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
||||
|
||||
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.
|
||||
|
||||
8
Makefile
8
Makefile
@@ -1,20 +1,21 @@
|
||||
BINARY_NAME=lambda
|
||||
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
|
||||
.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 " run - Build and run the lambda runtime (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 " lint - Run golangci-lint on all packages"
|
||||
echo " clean - Remove all build artifacts"
|
||||
|
||||
build:
|
||||
@@ -45,6 +46,9 @@ test:
|
||||
bench:
|
||||
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
|
||||
|
||||
lint:
|
||||
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
|
||||
|
||||
clean:
|
||||
rm -f ${BINARY_NAME}
|
||||
rm -f program.out
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# lambda
|
||||
|
||||
Making a lambda calculus interpreter in Go.
|
||||
Making a lambda calculus runtime in Go.
|
||||
|
||||
## Things to talk about
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/internal/engine"
|
||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
|
||||
func TestEngineEquivalence(t *testing.T) {
|
||||
testsDir := "../../tests"
|
||||
files, err := os.ReadDir(testsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read tests directory: %v", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasSuffix(file.Name(), ".test") {
|
||||
continue
|
||||
}
|
||||
|
||||
testName := strings.TrimSuffix(file.Name(), ".test")
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
// Read test input
|
||||
inputPath := filepath.Join(testsDir, file.Name())
|
||||
input, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read test file: %v", err)
|
||||
}
|
||||
|
||||
// Parse syntax tree
|
||||
ast, err := saccharine.Parse(string(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse input: %v", err)
|
||||
}
|
||||
|
||||
// Test lambda engine
|
||||
lambdaExpr := convert.SaccharineToLambda(ast)
|
||||
lambdaCfg := &config.Config{Interpreter: "lambda"}
|
||||
lambdaEngine := engine.NewLambdaEngine(lambdaCfg, &lambdaExpr)
|
||||
lambdaEngine.Run()
|
||||
lambdaResult := lambdaEngine.GetResult()
|
||||
|
||||
// Test De Bruijn engine
|
||||
debruijnExpr := convert.SaccharineToDeBruijn(ast)
|
||||
debruijnCfg := &config.Config{Interpreter: "debruijn"}
|
||||
debruijnEngine := engine.NewDeBruijnEngine(debruijnCfg, &debruijnExpr)
|
||||
debruijnEngine.Run()
|
||||
debruijnResult := debruijnEngine.GetResult()
|
||||
|
||||
// Convert De Bruijn result back to lambda for comparison
|
||||
debruijnConverted := convert.DeBruijnToLambda(*debruijnEngine.Expression)
|
||||
debruijnConvertedStr := convert.DeBruijnToLambda(*debruijnEngine.Expression)
|
||||
|
||||
// Check if expected file exists
|
||||
expectedPath := filepath.Join(testsDir, testName+".expected")
|
||||
if expectedBytes, err := os.ReadFile(expectedPath); err == nil {
|
||||
expected := strings.TrimSpace(string(expectedBytes))
|
||||
|
||||
if lambdaResult != expected {
|
||||
t.Errorf("Lambda engine result mismatch:\nExpected: %s\nGot: %s", expected, lambdaResult)
|
||||
}
|
||||
|
||||
// De Bruijn result will have different variable names, so we just check it runs
|
||||
if debruijnResult == "" {
|
||||
t.Errorf("De Bruijn engine produced empty result")
|
||||
}
|
||||
}
|
||||
|
||||
// Log results for comparison
|
||||
t.Logf("Lambda result: %s", lambdaResult)
|
||||
t.Logf("De Bruijn result: %s", debruijnResult)
|
||||
t.Logf("De Bruijn converted: %v", debruijnConvertedStr)
|
||||
_ = debruijnConverted // Suppress unused warning
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidInterpreterFlag(t *testing.T) {
|
||||
// This would be tested at the config level
|
||||
cfg := &config.Config{Interpreter: "invalid"}
|
||||
|
||||
// The validation happens in FromArgs, but we can test the engine creation
|
||||
// doesn't panic with invalid values
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Engine creation panicked with invalid interpreter: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Just check that default behavior works
|
||||
_ = cfg
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/cli"
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/internal/engine"
|
||||
"git.maximhutz.com/max/lambda/internal/performance"
|
||||
"git.maximhutz.com/max/lambda/internal/statistics"
|
||||
"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/normalorder"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
|
||||
@@ -33,92 +29,39 @@ func main() {
|
||||
cli.HandleError(err)
|
||||
logger.Info("parsed syntax tree", "tree", ast)
|
||||
|
||||
// Create reduction engine based on interpreter type.
|
||||
var process engine.Engine
|
||||
if options.Interpreter == "debruijn" {
|
||||
// Compile expression to De Bruijn indices.
|
||||
compiled := convert.SaccharineToDeBruijn(ast)
|
||||
logger.Info("compiled De Bruijn expression", "tree", debruijn.Stringify(compiled))
|
||||
dbEngine := engine.NewDeBruijnEngine(options, &compiled)
|
||||
// Compile expression to lambda calculus.
|
||||
compiled := convert.SaccharineToLambda(ast)
|
||||
logger.Info("compiled λ expression", "tree", compiled.String())
|
||||
|
||||
// If the user selected to track CPU performance, attach a profiler.
|
||||
if options.Profile != "" {
|
||||
profiler := performance.Track(options.Profile)
|
||||
dbEngine.On("start", profiler.Start)
|
||||
dbEngine.On("end", profiler.End)
|
||||
}
|
||||
// Create reducer with the compiled expression.
|
||||
runtime := normalorder.NewRuntime(compiled)
|
||||
|
||||
// If the user selected to produce a step-by-step explanation, print steps.
|
||||
if options.Explanation {
|
||||
dbEngine.On("start", func() {
|
||||
fmt.Println(debruijn.Stringify(*dbEngine.Expression))
|
||||
})
|
||||
dbEngine.On("step", func() {
|
||||
fmt.Println(" =", debruijn.Stringify(*dbEngine.Expression))
|
||||
})
|
||||
}
|
||||
|
||||
// If the user opted to track statistics, attach a tracker.
|
||||
if options.Statistics {
|
||||
statistics := statistics.Track()
|
||||
dbEngine.On("start", statistics.Start)
|
||||
dbEngine.On("step", statistics.Step)
|
||||
dbEngine.On("end", statistics.End)
|
||||
}
|
||||
|
||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||
if options.Verbose {
|
||||
dbEngine.On("step", func() {
|
||||
logger.Info("reduction", "tree", debruijn.Stringify(*dbEngine.Expression))
|
||||
})
|
||||
}
|
||||
|
||||
process = dbEngine
|
||||
} else {
|
||||
// Compile expression to lambda calculus.
|
||||
compiled := convert.SaccharineToLambda(ast)
|
||||
logger.Info("compiled λ expression", "tree", lambda.Stringify(compiled))
|
||||
lambdaEngine := engine.NewLambdaEngine(options, &compiled)
|
||||
|
||||
// If the user selected to track CPU performance, attach a profiler.
|
||||
if options.Profile != "" {
|
||||
profiler := performance.Track(options.Profile)
|
||||
lambdaEngine.On("start", profiler.Start)
|
||||
lambdaEngine.On("end", profiler.End)
|
||||
}
|
||||
|
||||
// If the user selected to produce a step-by-step explanation, print steps.
|
||||
if options.Explanation {
|
||||
lambdaEngine.On("start", func() {
|
||||
fmt.Println(lambda.Stringify(*lambdaEngine.Expression))
|
||||
})
|
||||
lambdaEngine.On("step", func() {
|
||||
fmt.Println(" =", lambda.Stringify(*lambdaEngine.Expression))
|
||||
})
|
||||
}
|
||||
|
||||
// If the user opted to track statistics, attach a tracker.
|
||||
if options.Statistics {
|
||||
statistics := statistics.Track()
|
||||
lambdaEngine.On("start", statistics.Start)
|
||||
lambdaEngine.On("step", statistics.Step)
|
||||
lambdaEngine.On("end", statistics.End)
|
||||
}
|
||||
|
||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||
if options.Verbose {
|
||||
lambdaEngine.On("step", func() {
|
||||
logger.Info("reduction", "tree", lambda.Stringify(*lambdaEngine.Expression))
|
||||
})
|
||||
}
|
||||
|
||||
process = lambdaEngine
|
||||
// If the user selected to track CPU performance, attach a profiler.
|
||||
if options.Profile != "" {
|
||||
plugins.NewPerformance(options.Profile, runtime)
|
||||
}
|
||||
|
||||
process.Run()
|
||||
// If the user selected to produce a step-by-step explanation, attach an
|
||||
// observer.
|
||||
if options.Explanation {
|
||||
plugins.NewExplanation(runtime)
|
||||
}
|
||||
|
||||
// If the user opted to track statistics, attach a tracker.
|
||||
if options.Statistics {
|
||||
plugins.NewStatistics(runtime)
|
||||
}
|
||||
|
||||
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||
if options.Verbose {
|
||||
plugins.NewLogs(logger, runtime)
|
||||
}
|
||||
|
||||
// Run reduction.
|
||||
runtime.Run()
|
||||
|
||||
// Return the final reduced result.
|
||||
result := process.GetResult()
|
||||
result := runtime.Expression().String()
|
||||
err = options.Destination.Write(result)
|
||||
cli.HandleError(err)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/internal/engine"
|
||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/normalorder"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to run a single sample through the lambda interpreter.
|
||||
// Helper function to run a single sample through the lambda runtime.
|
||||
func runSample(samplePath string) (string, error) {
|
||||
// Read the sample file.
|
||||
input, err := os.ReadFile(samplePath)
|
||||
@@ -31,21 +29,11 @@ func runSample(samplePath string) (string, error) {
|
||||
// Compile expression to lambda calculus.
|
||||
compiled := convert.SaccharineToLambda(ast)
|
||||
|
||||
// Create minimal config for benchmarking.
|
||||
cfg := &config.Config{
|
||||
Source: config.StringSource{Data: ""},
|
||||
Destination: config.StdoutDestination{},
|
||||
Profile: "",
|
||||
Explanation: false,
|
||||
Statistics: false,
|
||||
Verbose: false,
|
||||
}
|
||||
// Create and run the reducer.
|
||||
reducer := normalorder.NewRuntime(compiled)
|
||||
reducer.Run()
|
||||
|
||||
// Create and run the engine.
|
||||
process := engine.New(cfg, &compiled)
|
||||
process.Run()
|
||||
|
||||
return lambda.Stringify(compiled) + "\n", nil
|
||||
return reducer.Expression().String() + "\n", nil
|
||||
}
|
||||
|
||||
// Test that all samples produce expected output.
|
||||
|
||||
26
cmd/lambda/registry.go
Normal file
26
cmd/lambda/registry.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/internal/cli"
|
||||
"git.maximhutz.com/max/lambda/internal/registry"
|
||||
"git.maximhutz.com/max/lambda/pkg/convert"
|
||||
"git.maximhutz.com/max/lambda/pkg/engine/normalorder"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
|
||||
func MakeRegistry() *registry.Registry {
|
||||
r := registry.New()
|
||||
|
||||
// Codecs
|
||||
r.AddCodec(cli.ConvertCodec(convert.Saccharine2Lambda{}, "saccharine", "lambda"))
|
||||
|
||||
// Engines
|
||||
r.AddEngine(cli.ConvertEngine(normalorder.Engine{}, "normalorder", "lambda"))
|
||||
|
||||
// Marshalers
|
||||
r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda"))
|
||||
r.AddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine"))
|
||||
|
||||
return r
|
||||
}
|
||||
55
internal/cli/codec.go
Normal file
55
internal/cli/codec.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
)
|
||||
|
||||
type Codec interface {
|
||||
codec.Codec[Repr, Repr]
|
||||
|
||||
InType() string
|
||||
OutType() string
|
||||
}
|
||||
|
||||
type convertedCodec[T, U any] struct {
|
||||
codec codec.Codec[T, U]
|
||||
inType, outType string
|
||||
}
|
||||
|
||||
func (c convertedCodec[T, U]) Decode(r Repr) (Repr, error) {
|
||||
u, ok := r.Data().(U)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not parse '%v' as '%s'", r, c.inType)
|
||||
}
|
||||
|
||||
t, err := c.codec.Decode(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(c.outType, t), nil
|
||||
}
|
||||
|
||||
func (c convertedCodec[T, U]) Encode(r Repr) (Repr, error) {
|
||||
t, ok := r.Data().(T)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.outType)
|
||||
}
|
||||
|
||||
u, err := c.codec.Encode(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(c.inType, u), nil
|
||||
}
|
||||
|
||||
func (c convertedCodec[T, U]) InType() string { return c.inType }
|
||||
|
||||
func (c convertedCodec[T, U]) OutType() string { return c.outType }
|
||||
|
||||
func ConvertCodec[T, U any](e codec.Codec[T, U], inType, outType string) Codec {
|
||||
return convertedCodec[T, U]{e, inType, outType}
|
||||
}
|
||||
49
internal/cli/engine.go
Normal file
49
internal/cli/engine.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/engine"
|
||||
)
|
||||
|
||||
type Engine interface {
|
||||
engine.Engine[Repr]
|
||||
|
||||
Name() string
|
||||
InType() string
|
||||
}
|
||||
|
||||
type convertedEngine[T any] struct {
|
||||
engine engine.Engine[T]
|
||||
name string
|
||||
inType string
|
||||
}
|
||||
|
||||
func (b convertedEngine[T]) InType() string { return b.inType }
|
||||
|
||||
func (b convertedEngine[T]) Name() string { return b.name }
|
||||
|
||||
func (b convertedEngine[T]) Get() (Repr, error) {
|
||||
s, err := b.engine.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(b.inType, s), nil
|
||||
}
|
||||
|
||||
func (b convertedEngine[T]) Set(r Repr) error {
|
||||
if t, ok := r.Data().(T); ok {
|
||||
return b.engine.Set(t)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Incorrent format '%s' for engine '%s'.", r.Id(), b.inType)
|
||||
}
|
||||
|
||||
func (b convertedEngine[T]) Step(i int) bool {
|
||||
return b.engine.Step(i)
|
||||
}
|
||||
|
||||
func ConvertEngine[T any](e engine.Engine[T], name string, inType string) Engine {
|
||||
return convertedEngine[T]{e, name, inType}
|
||||
}
|
||||
42
internal/cli/marshaler.go
Normal file
42
internal/cli/marshaler.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
)
|
||||
|
||||
type Marshaler interface {
|
||||
codec.Marshaler[Repr]
|
||||
|
||||
InType() string
|
||||
}
|
||||
|
||||
type convertedMarshaler[T any] struct {
|
||||
codec codec.Marshaler[T]
|
||||
inType string
|
||||
}
|
||||
|
||||
func (c convertedMarshaler[T]) Decode(s string) (Repr, error) {
|
||||
t, err := c.codec.Decode(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewRepr(c.inType, t), nil
|
||||
}
|
||||
|
||||
func (c convertedMarshaler[T]) Encode(r Repr) (string, error) {
|
||||
t, ok := r.Data().(T)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("could not parse '%v' as 'string'", t)
|
||||
}
|
||||
|
||||
return c.codec.Encode(t)
|
||||
}
|
||||
|
||||
func (c convertedMarshaler[T]) InType() string { return c.inType }
|
||||
|
||||
func ConvertMarshaler[T any](e codec.Marshaler[T], inType string) Marshaler {
|
||||
return convertedMarshaler[T]{e, inType}
|
||||
}
|
||||
21
internal/cli/repr.go
Normal file
21
internal/cli/repr.go
Normal 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} }
|
||||
@@ -9,5 +9,4 @@ type Config struct {
|
||||
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 string // The interpreter to use: "lambda" or "debruijn".
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ func FromArgs() (*Config, error) {
|
||||
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. Choose 'lambda' or 'debruijn' reduction engine (default: lambda).")
|
||||
flag.Parse()
|
||||
|
||||
// Parse source type.
|
||||
@@ -46,11 +45,6 @@ func FromArgs() (*Config, error) {
|
||||
destination = FileDestination{Path: *output}
|
||||
}
|
||||
|
||||
// Validate interpreter flag.
|
||||
if *interpreter != "lambda" && *interpreter != "debruijn" {
|
||||
return nil, fmt.Errorf("invalid interpreter: %s (must be 'lambda' or 'debruijn')", *interpreter)
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
@@ -58,6 +52,5 @@ func FromArgs() (*Config, error) {
|
||||
Explanation: *explanation,
|
||||
Profile: *profile,
|
||||
Statistics: *statistics,
|
||||
Interpreter: *interpreter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
)
|
||||
|
||||
// A process for reducing one λ-expression using De Bruijn indices.
|
||||
type DeBruijnEngine struct {
|
||||
Config *config.Config
|
||||
Expression *debruijn.Expression
|
||||
emitter.Emitter
|
||||
}
|
||||
|
||||
// NewDeBruijnEngine creates a new De Bruijn engine.
|
||||
func NewDeBruijnEngine(config *config.Config, expression interface{}) *DeBruijnEngine {
|
||||
expr := expression.(*debruijn.Expression)
|
||||
return &DeBruijnEngine{Config: config, Expression: expr}
|
||||
}
|
||||
|
||||
// Run begins the reduction process.
|
||||
func (e *DeBruijnEngine) Run() {
|
||||
e.Emit("start")
|
||||
|
||||
debruijn.ReduceAll(e.Expression, func() {
|
||||
e.Emit("step")
|
||||
})
|
||||
|
||||
e.Emit("end")
|
||||
}
|
||||
|
||||
// GetResult returns the stringified result.
|
||||
func (e *DeBruijnEngine) GetResult() string {
|
||||
return debruijn.Stringify(*e.Expression)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Package "engine" provides an extensible interface for users to interfact with
|
||||
// λ-calculus.
|
||||
package engine
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
)
|
||||
|
||||
// Engine is an interface for reduction engines.
|
||||
type Engine interface {
|
||||
Run()
|
||||
GetResult() string
|
||||
On(message string, fn func()) *emitter.Observer
|
||||
Emit(message string)
|
||||
}
|
||||
|
||||
// New creates the appropriate engine based on the config.
|
||||
func New(cfg *config.Config, input interface{}) Engine {
|
||||
if cfg.Interpreter == "debruijn" {
|
||||
return NewDeBruijnEngine(cfg, input)
|
||||
}
|
||||
return NewLambdaEngine(cfg, input)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/internal/config"
|
||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
// A process for reducing one λ-expression using named variables.
|
||||
type LambdaEngine struct {
|
||||
Config *config.Config
|
||||
Expression *lambda.Expression
|
||||
emitter.Emitter
|
||||
}
|
||||
|
||||
// NewLambdaEngine creates a new lambda engine.
|
||||
func NewLambdaEngine(config *config.Config, expression interface{}) *LambdaEngine {
|
||||
expr := expression.(*lambda.Expression)
|
||||
return &LambdaEngine{Config: config, Expression: expr}
|
||||
}
|
||||
|
||||
// Run begins the reduction process.
|
||||
func (e *LambdaEngine) Run() {
|
||||
e.Emit("start")
|
||||
|
||||
lambda.ReduceAll(e.Expression, func() {
|
||||
e.Emit("step")
|
||||
})
|
||||
|
||||
e.Emit("end")
|
||||
}
|
||||
|
||||
// GetResult returns the stringified result.
|
||||
func (e *LambdaEngine) GetResult() string {
|
||||
return lambda.Stringify(*e.Expression)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Package "explanation" provides a observer to gather the reasoning during the
|
||||
// reduction, and present a thorough explanation to the user for each step.
|
||||
package explanation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/engine"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
// Track the reductions made by a reduction proess.
|
||||
type Tracker struct {
|
||||
process *engine.Engine
|
||||
}
|
||||
|
||||
// Attaches a new explanation tracker to a process.
|
||||
func Track(process *engine.Engine) *Tracker {
|
||||
tracker := &Tracker{process: process}
|
||||
process.On("start", tracker.Start)
|
||||
process.On("step", tracker.Step)
|
||||
|
||||
return tracker
|
||||
}
|
||||
|
||||
func (t *Tracker) Start() {
|
||||
fmt.Println(lambda.Stringify(*t.process.Expression))
|
||||
}
|
||||
|
||||
func (t *Tracker) Step() {
|
||||
fmt.Println(" =", lambda.Stringify(*t.process.Expression))
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Package "performance" provides a tracker to observer CPU performance during
|
||||
// execution.
|
||||
package performance
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
)
|
||||
|
||||
// Observes a reduction process, and publishes a CPU performance profile on
|
||||
// completion.
|
||||
type Tracker struct {
|
||||
File string
|
||||
filePointer *os.File
|
||||
Error error
|
||||
}
|
||||
|
||||
// Create a performance tracker that outputs a profile to "file".
|
||||
func Track(file string) *Tracker {
|
||||
return &Tracker{File: file}
|
||||
}
|
||||
|
||||
// Begin profiling.
|
||||
func (t *Tracker) Start() {
|
||||
var absPath string
|
||||
|
||||
absPath, t.Error = filepath.Abs(t.File)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Error = os.MkdirAll(filepath.Dir(absPath), 0777)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.filePointer, t.Error = os.Create(absPath)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Error = pprof.StartCPUProfile(t.filePointer)
|
||||
if t.Error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Stop profiling.
|
||||
func (t *Tracker) End() {
|
||||
pprof.StopCPUProfile()
|
||||
t.filePointer.Close()
|
||||
}
|
||||
75
internal/registry/registry.go
Normal file
75
internal/registry/registry.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/internal/cli"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
marshalers map[string]cli.Marshaler
|
||||
codecs []cli.Codec
|
||||
engines map[string]cli.Engine
|
||||
}
|
||||
|
||||
func New() *Registry {
|
||||
return &Registry{
|
||||
marshalers: map[string]cli.Marshaler{},
|
||||
codecs: []cli.Codec{},
|
||||
engines: map[string]cli.Engine{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) AddCodec(c cli.Codec) error {
|
||||
r.codecs = append(r.codecs, c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) AddMarshaler(c cli.Marshaler) error {
|
||||
if _, ok := r.marshalers[c.InType()]; ok {
|
||||
return fmt.Errorf("marshaler for '%s' already registered", c.InType())
|
||||
}
|
||||
|
||||
r.marshalers[c.InType()] = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) AddEngine(e cli.Engine) error {
|
||||
if _, ok := r.engines[e.Name()]; ok {
|
||||
return fmt.Errorf("engine '%s' already registered", e.Name())
|
||||
}
|
||||
|
||||
r.engines[e.Name()] = e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) GetEngine(name string) (cli.Engine, error) {
|
||||
e, ok := r.engines[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("engine '%s' not found", name)
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (r *Registry) ConvertTo(repr cli.Repr, outType string) (cli.Repr, error) {
|
||||
panic("")
|
||||
}
|
||||
|
||||
func (r *Registry) Marshal(repr cli.Repr) (string, error) {
|
||||
m, ok := r.marshalers[repr.Id()]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no marshaler for '%s'", repr.Id())
|
||||
}
|
||||
|
||||
return m.Encode(repr)
|
||||
}
|
||||
|
||||
func (r *Registry) Unmarshal(s string, outType string) (cli.Repr, error) {
|
||||
m, ok := r.marshalers[outType]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no marshaler for '%s'", outType)
|
||||
}
|
||||
|
||||
return m.Decode(s)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// An observer, to track reduction performance.
|
||||
type Tracker struct {
|
||||
start time.Time
|
||||
steps uint64
|
||||
}
|
||||
|
||||
// Create a new reduction performance tracker.
|
||||
func Track() *Tracker {
|
||||
return &Tracker{}
|
||||
}
|
||||
|
||||
func (t *Tracker) Start() {
|
||||
t.start = time.Now()
|
||||
t.steps = 0
|
||||
}
|
||||
|
||||
func (t *Tracker) Step() {
|
||||
t.steps++
|
||||
}
|
||||
|
||||
func (t *Tracker) End() {
|
||||
results := Results{
|
||||
StepsTaken: t.steps,
|
||||
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
||||
}
|
||||
|
||||
fmt.Fprint(os.Stderr, results.String())
|
||||
}
|
||||
8
pkg/codec/codec.go
Normal file
8
pkg/codec/codec.go
Normal 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]
|
||||
@@ -1,63 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
// DeBruijnToLambda converts a De Bruijn expression back to named lambda calculus.
|
||||
func DeBruijnToLambda(expr debruijn.Expression) lambda.Expression {
|
||||
return deBruijnToLambda(expr, []string{})
|
||||
}
|
||||
|
||||
func deBruijnToLambda(expr debruijn.Expression, context []string) lambda.Expression {
|
||||
switch e := expr.(type) {
|
||||
case *debruijn.Variable:
|
||||
if e.Index() >= 0 && e.Index() < len(context) {
|
||||
return lambda.NewVariable(context[e.Index()])
|
||||
}
|
||||
if e.Label() != "" {
|
||||
return lambda.NewVariable(e.Label())
|
||||
}
|
||||
return lambda.NewVariable(fmt.Sprintf("free_%d", e.Index()))
|
||||
|
||||
case *debruijn.Abstraction:
|
||||
paramName := generateParamName(context)
|
||||
newContext := append([]string{paramName}, context...)
|
||||
body := deBruijnToLambda(e.Body(), newContext)
|
||||
return lambda.NewAbstraction(paramName, body)
|
||||
|
||||
case *debruijn.Application:
|
||||
abs := deBruijnToLambda(e.Abstraction(), context)
|
||||
arg := deBruijnToLambda(e.Argument(), context)
|
||||
return lambda.NewApplication(abs, arg)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateParamName generates a fresh parameter name that doesn't conflict with context.
|
||||
func generateParamName(context []string) string {
|
||||
base := 'a'
|
||||
for i := 0; ; i++ {
|
||||
name := string(rune(base + rune(i%26)))
|
||||
if i >= 26 {
|
||||
name = fmt.Sprintf("%s%d", name, i/26)
|
||||
}
|
||||
|
||||
conflict := false
|
||||
for _, existing := range context {
|
||||
if existing == name {
|
||||
conflict = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !conflict {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
// LambdaToDeBruijn converts a lambda expression to De Bruijn index representation.
|
||||
func LambdaToDeBruijn(expr lambda.Expression) debruijn.Expression {
|
||||
return lambdaToDeBruijn(expr, []string{})
|
||||
}
|
||||
|
||||
func lambdaToDeBruijn(expr lambda.Expression, context []string) debruijn.Expression {
|
||||
switch e := expr.(type) {
|
||||
case *lambda.Variable:
|
||||
index := findIndex(e.Value(), context)
|
||||
return debruijn.NewVariable(index, e.Value())
|
||||
|
||||
case *lambda.Abstraction:
|
||||
newContext := append([]string{e.Parameter()}, context...)
|
||||
body := lambdaToDeBruijn(e.Body(), newContext)
|
||||
return debruijn.NewAbstraction(body)
|
||||
|
||||
case *lambda.Application:
|
||||
abs := lambdaToDeBruijn(e.Abstraction(), context)
|
||||
arg := lambdaToDeBruijn(e.Argument(), context)
|
||||
return debruijn.NewApplication(abs, arg)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// findIndex returns the De Bruijn index for a variable name in the context.
|
||||
// Returns the index if found, or -1 if the variable is free.
|
||||
func findIndex(name string, context []string) int {
|
||||
for i, v := range context {
|
||||
if v == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/debruijn"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||
)
|
||||
|
||||
// SaccharineToDeBruijn converts a saccharine expression directly to De Bruijn indices.
|
||||
func SaccharineToDeBruijn(expr saccharine.Expression) debruijn.Expression {
|
||||
lambdaExpr := SaccharineToLambda(expr)
|
||||
return LambdaToDeBruijn(lambdaExpr)
|
||||
}
|
||||
@@ -3,23 +3,24 @@ package convert
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/codec"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
"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)
|
||||
}
|
||||
|
||||
func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||
result := SaccharineToLambda(n.Body)
|
||||
func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||
result := encodeExpression(n.Body)
|
||||
|
||||
parameters := n.Parameters
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -31,13 +32,13 @@ func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||
return result
|
||||
}
|
||||
|
||||
func convertApplication(n *saccharine.Application) lambda.Expression {
|
||||
result := SaccharineToLambda(n.Abstraction)
|
||||
func encodeApplication(n *saccharine.Application) lambda.Expression {
|
||||
result := encodeExpression(n.Abstraction)
|
||||
|
||||
arguments := []lambda.Expression{}
|
||||
for _, argument := range n.Arguments {
|
||||
convertedArgument := SaccharineToLambda(argument)
|
||||
arguments = append(arguments, convertedArgument)
|
||||
encodeedArgument := encodeExpression(argument)
|
||||
arguments = append(arguments, encodeedArgument)
|
||||
}
|
||||
|
||||
for _, argument := range arguments {
|
||||
@@ -51,9 +52,9 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
|
||||
var value lambda.Expression
|
||||
|
||||
if len(s.Parameters) == 0 {
|
||||
value = SaccharineToLambda(s.Body)
|
||||
value = encodeExpression(s.Body)
|
||||
} else {
|
||||
value = convertAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
|
||||
value = encodeAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
|
||||
}
|
||||
|
||||
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 {
|
||||
freshVar := lambda.GenerateFreshName(lambda.GetFreeVariables(e))
|
||||
freshVar := lambda.GenerateFreshName(e.GetFree())
|
||||
|
||||
return lambda.NewApplication(
|
||||
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 {
|
||||
result := SaccharineToLambda(n.Returns)
|
||||
func encodeClause(n *saccharine.Clause) lambda.Expression {
|
||||
result := encodeExpression(n.Returns)
|
||||
|
||||
for i := len(n.Statements) - 1; i >= 0; i-- {
|
||||
result = reduceStatement(n.Statements[i], result)
|
||||
@@ -92,17 +93,46 @@ func convertClause(n *saccharine.Clause) lambda.Expression {
|
||||
return result
|
||||
}
|
||||
|
||||
func SaccharineToLambda(n saccharine.Expression) lambda.Expression {
|
||||
switch n := n.(type) {
|
||||
func encodeExpression(s saccharine.Expression) lambda.Expression {
|
||||
switch s := s.(type) {
|
||||
case *saccharine.Atom:
|
||||
return convertAtom(n)
|
||||
return encodeAtom(s)
|
||||
case *saccharine.Abstraction:
|
||||
return convertAbstraction(n)
|
||||
return encodeAbstraction(s)
|
||||
case *saccharine.Application:
|
||||
return convertApplication(n)
|
||||
return encodeApplication(s)
|
||||
case *saccharine.Clause:
|
||||
return convertClause(n)
|
||||
return encodeClause(s)
|
||||
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)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package debruijn
|
||||
|
||||
type Expression interface {
|
||||
Accept(Visitor)
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Abstraction struct {
|
||||
body Expression
|
||||
}
|
||||
|
||||
func (a *Abstraction) Body() Expression {
|
||||
return a.body
|
||||
}
|
||||
|
||||
func (a *Abstraction) Accept(v Visitor) {
|
||||
v.VisitAbstraction(a)
|
||||
}
|
||||
|
||||
func NewAbstraction(body Expression) *Abstraction {
|
||||
return &Abstraction{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 NewApplication(abstraction Expression, argument Expression) *Application {
|
||||
return &Application{abstraction: abstraction, argument: argument}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Variable struct {
|
||||
index int
|
||||
label string
|
||||
}
|
||||
|
||||
func (v *Variable) Index() int {
|
||||
return v.index
|
||||
}
|
||||
|
||||
func (v *Variable) Label() string {
|
||||
return v.label
|
||||
}
|
||||
|
||||
func (v *Variable) Accept(visitor Visitor) {
|
||||
visitor.VisitVariable(v)
|
||||
}
|
||||
|
||||
func NewVariable(index int, label string) *Variable {
|
||||
return &Variable{index: index, label: label}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Visitor interface {
|
||||
VisitAbstraction(*Abstraction)
|
||||
VisitApplication(*Application)
|
||||
VisitVariable(*Variable)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package debruijn
|
||||
|
||||
type Iterator struct {
|
||||
trace []*Expression
|
||||
}
|
||||
|
||||
func NewIterator(expr *Expression) *Iterator {
|
||||
return &Iterator{[]*Expression{expr}}
|
||||
}
|
||||
|
||||
func (i *Iterator) Done() bool {
|
||||
return len(i.trace) == 0
|
||||
}
|
||||
|
||||
func (i *Iterator) Current() *Expression {
|
||||
if i.Done() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return i.trace[len(i.trace)-1]
|
||||
}
|
||||
|
||||
func (i *Iterator) Parent() *Expression {
|
||||
if len(i.trace) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return i.trace[len(i.trace)-2]
|
||||
}
|
||||
|
||||
func (i *Iterator) Swap(with Expression) {
|
||||
current := i.Current()
|
||||
if current != nil {
|
||||
*current = with
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Iterator) Back() bool {
|
||||
if i.Done() {
|
||||
return false
|
||||
}
|
||||
|
||||
i.trace = i.trace[:len(i.trace)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
func (i *Iterator) Next() {
|
||||
switch typed := (*i.Current()).(type) {
|
||||
case *Abstraction:
|
||||
i.trace = append(i.trace, &typed.body)
|
||||
case *Application:
|
||||
i.trace = append(i.trace, &typed.abstraction)
|
||||
case *Variable:
|
||||
for len(i.trace) > 1 {
|
||||
if app, ok := (*i.Parent()).(*Application); ok {
|
||||
if app.abstraction == *i.Current() {
|
||||
i.Back()
|
||||
i.trace = append(i.trace, &app.argument)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
i.Back()
|
||||
}
|
||||
|
||||
i.trace = []*Expression{}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package debruijn
|
||||
|
||||
func IsViable(e *Expression) (*Abstraction, Expression, bool) {
|
||||
if e == nil {
|
||||
return nil, nil, false
|
||||
} else if app, appOk := (*e).(*Application); !appOk {
|
||||
return nil, nil, false
|
||||
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
|
||||
return nil, nil, false
|
||||
} else {
|
||||
return fn, app.argument, true
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceAll(e *Expression, step func()) {
|
||||
it := NewIterator(e)
|
||||
|
||||
for !it.Done() {
|
||||
if fn, arg, ok := IsViable(it.Current()); !ok {
|
||||
it.Next()
|
||||
} else {
|
||||
it.Swap(Substitute(fn.body, arg))
|
||||
step()
|
||||
|
||||
if _, _, ok := IsViable(it.Parent()); ok {
|
||||
it.Back()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package debruijn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type stringifyVisitor struct {
|
||||
builder strings.Builder
|
||||
}
|
||||
|
||||
func (v *stringifyVisitor) VisitVariable(a *Variable) {
|
||||
if a.label != "" {
|
||||
v.builder.WriteString(a.label)
|
||||
} else {
|
||||
v.builder.WriteString(fmt.Sprintf("%d", 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(')')
|
||||
}
|
||||
|
||||
func Stringify(e Expression) string {
|
||||
b := &stringifyVisitor{builder: strings.Builder{}}
|
||||
e.Accept(b)
|
||||
return b.builder.String()
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package debruijn
|
||||
|
||||
// Shift increments all free variable indices by delta when crossing depth abstractions.
|
||||
func Shift(expr Expression, delta int, depth int) Expression {
|
||||
switch e := expr.(type) {
|
||||
case *Variable:
|
||||
if e.index >= depth {
|
||||
return NewVariable(e.index+delta, e.label)
|
||||
}
|
||||
return e
|
||||
|
||||
case *Abstraction:
|
||||
newBody := Shift(e.body, delta, depth+1)
|
||||
if newBody == e.body {
|
||||
return e
|
||||
}
|
||||
return NewAbstraction(newBody)
|
||||
|
||||
case *Application:
|
||||
newAbs := Shift(e.abstraction, delta, depth)
|
||||
newArg := Shift(e.argument, delta, depth)
|
||||
if newAbs == e.abstraction && newArg == e.argument {
|
||||
return e
|
||||
}
|
||||
return NewApplication(newAbs, newArg)
|
||||
|
||||
default:
|
||||
return expr
|
||||
}
|
||||
}
|
||||
|
||||
// Substitute replaces variable at index 0 with replacement in expr.
|
||||
// This assumes expr is the body of an abstraction being applied.
|
||||
func Substitute(expr Expression, replacement Expression) Expression {
|
||||
return substitute(expr, 0, replacement)
|
||||
}
|
||||
|
||||
// substitute replaces variable at targetIndex with replacement, adjusting indices as needed.
|
||||
func substitute(expr Expression, targetIndex int, replacement Expression) Expression {
|
||||
switch e := expr.(type) {
|
||||
case *Variable:
|
||||
if e.index == targetIndex {
|
||||
return Shift(replacement, targetIndex, 0)
|
||||
}
|
||||
if e.index > targetIndex {
|
||||
return NewVariable(e.index-1, e.label)
|
||||
}
|
||||
return e
|
||||
|
||||
case *Abstraction:
|
||||
newBody := substitute(e.body, targetIndex+1, replacement)
|
||||
if newBody == e.body {
|
||||
return e
|
||||
}
|
||||
return NewAbstraction(newBody)
|
||||
|
||||
case *Application:
|
||||
newAbs := substitute(e.abstraction, targetIndex, replacement)
|
||||
newArg := substitute(e.argument, targetIndex, replacement)
|
||||
if newAbs == e.abstraction && newArg == e.argument {
|
||||
return e
|
||||
}
|
||||
return NewApplication(newAbs, newArg)
|
||||
|
||||
default:
|
||||
return expr
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Package "deltanet" is a reduction strategy using ∆-nets.
|
||||
package deltanet
|
||||
|
||||
type Graph struct {
|
||||
Nodes []Node
|
||||
}
|
||||
@@ -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{} }
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
@@ -2,53 +2,45 @@ package emitter
|
||||
|
||||
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||
|
||||
type Observer struct {
|
||||
fn func()
|
||||
message string
|
||||
emitter *Emitter
|
||||
type Emitter[E comparable] interface {
|
||||
On(E, func()) Listener[E]
|
||||
Off(Listener[E])
|
||||
Emit(E)
|
||||
}
|
||||
|
||||
type Emitter struct {
|
||||
listeners map[string]*set.Set[*Observer]
|
||||
type BaseEmitter[E comparable] struct {
|
||||
listeners map[E]set.Set[Listener[E]]
|
||||
}
|
||||
|
||||
func Ignore[T any](fn func()) func(T) {
|
||||
return func(T) { fn() }
|
||||
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 *Emitter) On(message string, fn func()) *Observer {
|
||||
observer := &Observer{
|
||||
fn: fn,
|
||||
message: message,
|
||||
emitter: e,
|
||||
}
|
||||
|
||||
if e.listeners == nil {
|
||||
e.listeners = map[string]*set.Set[*Observer]{}
|
||||
}
|
||||
|
||||
if e.listeners[message] == nil {
|
||||
e.listeners[message] = set.New[*Observer]()
|
||||
}
|
||||
|
||||
e.listeners[message].Add(observer)
|
||||
return observer
|
||||
}
|
||||
|
||||
func (o *Observer) Off() {
|
||||
if o.emitter.listeners[o.message] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
o.emitter.listeners[o.message].Remove(o)
|
||||
}
|
||||
|
||||
func (e *Emitter) Emit(message string) {
|
||||
if e.listeners[message] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for listener := range *e.listeners[message] {
|
||||
listener.fn()
|
||||
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]]{},
|
||||
}
|
||||
}
|
||||
|
||||
19
pkg/emitter/listener.go
Normal file
19
pkg/emitter/listener.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package emitter
|
||||
|
||||
type Listener[E comparable] interface {
|
||||
Kind() E
|
||||
Run()
|
||||
}
|
||||
|
||||
type BaseListener[E comparable] struct {
|
||||
kind E
|
||||
fn func()
|
||||
}
|
||||
|
||||
func (l BaseListener[E]) Kind() E {
|
||||
return l.kind
|
||||
}
|
||||
|
||||
func (l BaseListener[E]) Run() {
|
||||
l.fn()
|
||||
}
|
||||
7
pkg/engine/engine.go
Normal file
7
pkg/engine/engine.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package engine
|
||||
|
||||
type Engine[T any] interface {
|
||||
Get() (T, error)
|
||||
Set(T) error
|
||||
Step(int) bool
|
||||
}
|
||||
34
pkg/engine/normalorder/engine.go
Normal file
34
pkg/engine/normalorder/engine.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package normalorder
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/engine"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
expr lambda.Expression
|
||||
}
|
||||
|
||||
func (e Engine) Get() (lambda.Expression, error) {
|
||||
return e.expr, nil
|
||||
}
|
||||
|
||||
func (e Engine) Set(l lambda.Expression) error {
|
||||
e.expr = l
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Engine) Step(i int) bool {
|
||||
var reduced bool
|
||||
|
||||
for range i {
|
||||
e.expr, reduced = ReduceOnce(e.expr)
|
||||
if !reduced {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var _ engine.Engine[lambda.Expression] = (*Engine)(nil)
|
||||
34
pkg/engine/normalorder/reduce_one.go
Normal file
34
pkg/engine/normalorder/reduce_one.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,32 @@
|
||||
package lambda
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/set"
|
||||
)
|
||||
|
||||
// Expression is the interface for all lambda calculus expression types.
|
||||
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
||||
type Expression interface {
|
||||
Accept(Visitor)
|
||||
fmt.Stringer
|
||||
|
||||
// Substitute replaces all free occurrences of the target variable with the
|
||||
// replacement expression. Alpha-renaming is performed automatically to
|
||||
// avoid variable capture.
|
||||
Substitute(target string, replacement Expression) Expression
|
||||
|
||||
// GetFree returns the set of all free variable names in the expression.
|
||||
// This function does not mutate the input expression.
|
||||
// The returned set is newly allocated and can be modified by the caller.
|
||||
GetFree() set.Set[string]
|
||||
|
||||
// Rename replaces all occurrences of the target variable name with the new name.
|
||||
Rename(target string, newName string) Expression
|
||||
|
||||
// IsFree returns true if the variable name n occurs free in the expression.
|
||||
// This function does not mutate the input expression.
|
||||
IsFree(n string) bool
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
@@ -11,20 +36,22 @@ type Abstraction struct {
|
||||
body Expression
|
||||
}
|
||||
|
||||
func (a *Abstraction) Parameter() string {
|
||||
var _ Expression = Abstraction{}
|
||||
|
||||
func (a Abstraction) Parameter() string {
|
||||
return a.parameter
|
||||
}
|
||||
|
||||
func (a *Abstraction) Body() Expression {
|
||||
func (a Abstraction) Body() Expression {
|
||||
return a.body
|
||||
}
|
||||
|
||||
func (a *Abstraction) Accept(v Visitor) {
|
||||
v.VisitAbstraction(a)
|
||||
func (a Abstraction) String() string {
|
||||
return "\\" + a.parameter + "." + a.body.String()
|
||||
}
|
||||
|
||||
func NewAbstraction(parameter string, body Expression) *Abstraction {
|
||||
return &Abstraction{parameter: parameter, body: body}
|
||||
func NewAbstraction(parameter string, body Expression) Abstraction {
|
||||
return Abstraction{parameter, body}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
@@ -34,44 +61,40 @@ type Application struct {
|
||||
argument Expression
|
||||
}
|
||||
|
||||
func (a *Application) Abstraction() Expression {
|
||||
var _ Expression = Application{}
|
||||
|
||||
func (a Application) Abstraction() Expression {
|
||||
return a.abstraction
|
||||
}
|
||||
|
||||
func (a *Application) Argument() Expression {
|
||||
func (a Application) Argument() Expression {
|
||||
return a.argument
|
||||
}
|
||||
|
||||
func (a *Application) Accept(v Visitor) {
|
||||
v.VisitApplication(a)
|
||||
func (a Application) String() string {
|
||||
return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
|
||||
}
|
||||
|
||||
func NewApplication(abstraction Expression, argument Expression) *Application {
|
||||
return &Application{abstraction: abstraction, argument: argument}
|
||||
func NewApplication(abstraction Expression, argument Expression) Application {
|
||||
return Application{abstraction, argument}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Variable struct {
|
||||
value string
|
||||
name string
|
||||
}
|
||||
|
||||
func (v *Variable) Value() string {
|
||||
return v.value
|
||||
var _ Expression = Variable{}
|
||||
|
||||
func (v Variable) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func (v *Variable) Accept(visitor Visitor) {
|
||||
visitor.VisitVariable(v)
|
||||
func (v Variable) String() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
func NewVariable(name string) *Variable {
|
||||
return &Variable{value: name}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------------- */
|
||||
|
||||
type Visitor interface {
|
||||
VisitAbstraction(*Abstraction)
|
||||
VisitApplication(*Application)
|
||||
VisitVariable(*Variable)
|
||||
func NewVariable(name string) Variable {
|
||||
return Variable{name}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -2,19 +2,18 @@ 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
19
pkg/lambda/marshaler.go
Normal 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)
|
||||
@@ -1,30 +0,0 @@
|
||||
package lambda
|
||||
|
||||
func IsViable(e *Expression) (*Abstraction, Expression, bool) {
|
||||
if e == nil {
|
||||
return nil, nil, false
|
||||
} else if app, appOk := (*e).(*Application); !appOk {
|
||||
return nil, nil, false
|
||||
} else if fn, fnOk := app.abstraction.(*Abstraction); !fnOk {
|
||||
return nil, nil, false
|
||||
} else {
|
||||
return fn, app.argument, true
|
||||
}
|
||||
}
|
||||
|
||||
func ReduceAll(e *Expression, step func()) {
|
||||
it := NewIterator(e)
|
||||
|
||||
for !it.Done() {
|
||||
if fn, arg, ok := IsViable(it.Current()); !ok {
|
||||
it.Next()
|
||||
} else {
|
||||
it.Swap(Substitute(fn.body, fn.parameter, arg))
|
||||
step()
|
||||
|
||||
if _, _, ok := IsViable(it.Parent()); ok {
|
||||
it.Back()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,28 @@
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,46 +1,35 @@
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
24
pkg/saccharine/marshaler.go
Normal file
24
pkg/saccharine/marshaler.go
Normal 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)
|
||||
@@ -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 TokenType) (*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'", expected.Name(), 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 TokenType, 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
|
||||
@@ -105,7 +104,7 @@ func parseApplication(i *TokenIterator) (*Application, 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())
|
||||
} else {
|
||||
return NewAtom(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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -56,27 +56,27 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
|
||||
switch {
|
||||
case letter == '(':
|
||||
return NewOpenParen(index), nil
|
||||
return NewTokenOpenParen(index), nil
|
||||
case letter == ')':
|
||||
return NewCloseParen(index), nil
|
||||
return NewTokenCloseParen(index), nil
|
||||
case letter == '.':
|
||||
return NewDot(index), nil
|
||||
return NewTokenDot(index), nil
|
||||
case letter == '\\':
|
||||
return NewSlash(index), nil
|
||||
return NewTokenSlash(index), nil
|
||||
case letter == '\n':
|
||||
return NewSoftBreak(index), nil
|
||||
return NewTokenSoftBreak(index), nil
|
||||
case letter == '{':
|
||||
return NewOpenBrace(index), nil
|
||||
return NewTokenOpenBrace(index), nil
|
||||
case letter == '}':
|
||||
return NewCloseBrace(index), nil
|
||||
return NewTokenCloseBrace(index), nil
|
||||
case letter == ':':
|
||||
if _, err := parseCharacter(i, '='); err != nil {
|
||||
if _, err := scanCharacter(i, '='); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return NewAssign(index), nil
|
||||
return NewTokenAssign(index), nil
|
||||
}
|
||||
case letter == ';':
|
||||
return NewHardBreak(index), nil
|
||||
return NewTokenHardBreak(index), nil
|
||||
case letter == '#':
|
||||
// Skip everything until the next newline or EOF.
|
||||
for !i.Done() {
|
||||
@@ -98,27 +98,27 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
return NewAtom(string(atom), index), nil
|
||||
return NewTokenAtom(string(atom), index), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown character '%v'", string(letter))
|
||||
}
|
||||
|
||||
// Parse a string into tokens.
|
||||
func Parse(input string) ([]Token, error) {
|
||||
// scan a string into tokens.
|
||||
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 {
|
||||
91
pkg/saccharine/token.go
Normal file
91
pkg/saccharine/token.go
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package set
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,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)
|
||||
}
|
||||
}
|
||||
@@ -34,8 +36,18 @@ func (s Set[T]) ToList() []T {
|
||||
return list
|
||||
}
|
||||
|
||||
func New[T comparable](items ...T) *Set[T] {
|
||||
result := &Set[T]{}
|
||||
func (s Set[T]) Items() iter.Seq[T] {
|
||||
return func(yield func(T) bool) {
|
||||
for item := range s {
|
||||
if !yield(item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func New[T comparable](items ...T) Set[T] {
|
||||
result := Set[T]{}
|
||||
|
||||
for _, item := range items {
|
||||
result.Add(item)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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))
|
||||
Reference in New Issue
Block a user