4 Commits

Author SHA1 Message Date
5ccc41b104 feat: add test target to Makefile.
Added make test directive that runs tests without benchmarks.
Updated help text to include the new test target.
2026-01-12 20:13:14 -05:00
e17a85e0a3 refactor: use assert throughout tests and require expected files.
Renamed benchmark_test.go to lambda_test.go.
Consolidated helper functions to use single runSample function.
Replaced all error handling with assert for consistency.
Removed optional expected file check to require all test files have corresponding expected files.
2026-01-12 20:09:21 -05:00
4a5c424e54 test: add dynamic test discovery and validity checks.
Modified benchmark_test.go to dynamically discover all .test files in the
tests directory instead of using hardcoded paths.
Added TestSamplesValidity integration test that validates each test file
against its corresponding .expected file.
Added runSampleWithOutput helper function to capture interpreter output.
Added new test cases with expected outputs for validation.
2026-01-12 20:04:00 -05:00
588f4cd521 feat: added swap to iterator 2026-01-12 19:39:48 -05:00
51 changed files with 821 additions and 1034 deletions

View File

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

View File

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

View File

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

View File

@@ -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,9 +126,6 @@ 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:

View File

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

View File

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

View File

@@ -1,21 +1,20 @@
BINARY_NAME=lambda
TEST=simple
.PHONY: help build run profile explain graph docs test bench lint clean
.PHONY: help build run profile explain graph docs test bench clean
.DEFAULT_GOAL := help
.SILENT:
help:
echo "Available targets:"
echo " build - Build the lambda executable"
echo " run - Build and run the lambda runtime (use TEST=<name> to specify sample)"
echo " run - Build and run the lambda interpreter (use TEST=<name> to specify sample)"
echo " profile - Build and run with CPU profiling enabled"
echo " explain - Build and run with explanation mode and profiling"
echo " graph - Generate and open CPU profile visualization"
echo " docs - Start local godoc server on port 6060"
echo " test - Run tests for all samples"
echo " bench - Run benchmarks for all samples"
echo " lint - Run golangci-lint on all packages"
echo " clean - Remove all build artifacts"
build:
@@ -46,9 +45,6 @@ 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

View File

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

View File

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

View File

@@ -6,13 +6,15 @@ 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/normalorder"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/saccharine"
"github.com/stretchr/testify/assert"
)
// Helper function to run a single sample through the lambda runtime.
// Helper function to run a single sample through the lambda interpreter.
func runSample(samplePath string) (string, error) {
// Read the sample file.
input, err := os.ReadFile(samplePath)
@@ -29,11 +31,21 @@ func runSample(samplePath string) (string, error) {
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
// Create and run the reducer.
reducer := normalorder.NewRuntime(compiled)
reducer.Run()
// Create minimal config for benchmarking.
cfg := &config.Config{
Source: config.StringSource{Data: ""},
Destination: config.StdoutDestination{},
Profile: "",
Explanation: false,
Statistics: false,
Verbose: false,
}
return reducer.Expression().String() + "\n", nil
// Create and run the engine.
process := engine.New(cfg, &compiled)
process.Run()
return lambda.Stringify(compiled) + "\n", nil
}
// Test that all samples produce expected output.

View File

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

View File

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

View File

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

View File

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

View File

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

32
internal/engine/engine.go Normal file
View File

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

View File

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

View File

@@ -0,0 +1,53 @@
// Package "performance" provides a tracker to observer CPU performance during
// execution.
package performance
import (
"os"
"path/filepath"
"runtime/pprof"
)
// Observes a reduction process, and publishes a CPU performance profile on
// completion.
type Tracker struct {
File string
filePointer *os.File
Error error
}
// Create a performance tracker that outputs a profile to "file".
func Track(file string) *Tracker {
return &Tracker{File: file}
}
// Begin profiling.
func (t *Tracker) Start() {
var absPath string
absPath, t.Error = filepath.Abs(t.File)
if t.Error != nil {
return
}
t.Error = os.MkdirAll(filepath.Dir(absPath), 0777)
if t.Error != nil {
return
}
t.filePointer, t.Error = os.Create(absPath)
if t.Error != nil {
return
}
t.Error = pprof.StartCPUProfile(t.filePointer)
if t.Error != nil {
return
}
}
// Stop profiling.
func (t *Tracker) End() {
pprof.StopCPUProfile()
t.filePointer.Close()
}

View File

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

View File

@@ -0,0 +1,28 @@
// Package "statistics" provides a way to observer reduction speed during
// execution.
package statistics
import (
"fmt"
"strings"
)
// Statistics for a specific reduction.
type Results struct {
StepsTaken uint64 // Number of steps taken during execution.
TimeElapsed uint64 // The time (ms) taken for execution to complete.
}
// Returns the average number of operations per second of the execution.
func (r Results) OpsPerSecond() float32 {
return float32(r.StepsTaken) / (float32(r.TimeElapsed) / 1000)
}
// Format the results as a string.
func (r Results) String() string {
builder := strings.Builder{}
fmt.Fprintln(&builder, "Time Spent:", r.TimeElapsed, "ms")
fmt.Fprintln(&builder, "Steps:", r.StepsTaken)
fmt.Fprintln(&builder, "Speed:", r.OpsPerSecond(), "ops")
return builder.String()
}

View File

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

View File

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

View File

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

6
pkg/deltanet/deltanet.go Normal file
View File

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

94
pkg/deltanet/node.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,7 @@
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 {
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
Accept(Visitor)
}
/** ------------------------------------------------------------------------- */
@@ -36,22 +11,20 @@ type Abstraction struct {
body Expression
}
var _ Expression = Abstraction{}
func (a Abstraction) Parameter() string {
func (a *Abstraction) Parameter() string {
return a.parameter
}
func (a Abstraction) Body() Expression {
func (a *Abstraction) Body() Expression {
return a.body
}
func (a Abstraction) String() string {
return "\\" + a.parameter + "." + a.body.String()
func (a *Abstraction) Accept(v Visitor) {
v.VisitAbstraction(a)
}
func NewAbstraction(parameter string, body Expression) Abstraction {
return Abstraction{parameter, body}
func NewAbstraction(parameter string, body Expression) *Abstraction {
return &Abstraction{parameter: parameter, body: body}
}
/** ------------------------------------------------------------------------- */
@@ -61,40 +34,44 @@ type Application struct {
argument Expression
}
var _ Expression = Application{}
func (a Application) Abstraction() Expression {
func (a *Application) Abstraction() Expression {
return a.abstraction
}
func (a Application) Argument() Expression {
func (a *Application) Argument() Expression {
return a.argument
}
func (a Application) String() string {
return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
func (a *Application) Accept(v Visitor) {
v.VisitApplication(a)
}
func NewApplication(abstraction Expression, argument Expression) Application {
return Application{abstraction, argument}
func NewApplication(abstraction Expression, argument Expression) *Application {
return &Application{abstraction: abstraction, argument: argument}
}
/** ------------------------------------------------------------------------- */
type Variable struct {
name string
value string
}
var _ Expression = Variable{}
func (v Variable) Name() string {
return v.name
func (v *Variable) Value() string {
return v.value
}
func (v Variable) String() string {
return v.name
func (v *Variable) Accept(visitor Visitor) {
visitor.VisitVariable(v)
}
func NewVariable(name string) Variable {
return Variable{name}
func NewVariable(name string) *Variable {
return &Variable{value: name}
}
/** ------------------------------------------------------------------------- */
type Visitor interface {
VisitAbstraction(*Abstraction)
VisitApplication(*Application)
VisitVariable(*Variable)
}

View File

@@ -6,9 +6,7 @@ import (
"git.maximhutz.com/max/lambda/pkg/set"
)
// 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 {
func GenerateFreshName(used *set.Set[string]) string {
for i := uint64(0); ; i++ {
attempt := "_" + string(strconv.AppendUint(nil, i, 10))

View File

@@ -2,18 +2,19 @@ package lambda
import "git.maximhutz.com/max/lambda/pkg/set"
func (e Variable) GetFree() set.Set[string] {
return set.New(e.Name())
}
func (e Abstraction) GetFree() set.Set[string] {
vars := e.Body().GetFree()
vars.Remove(e.Parameter())
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
}
func (e Application) GetFree() set.Set[string] {
vars := e.Abstraction().GetFree()
vars.Merge(e.Argument().GetFree())
case *Application:
vars := GetFreeVariables(e.abstraction)
vars.Merge(GetFreeVariables(e.argument))
return vars
default:
return nil
}
}

View File

@@ -1,12 +1,14 @@
package lambda
func (e Variable) IsFree(n string) bool {
return e.Name() == n
}
func (e Abstraction) IsFree(n string) bool {
return e.Parameter() != n && e.Body().IsFree(n)
}
func (e Application) IsFree(n string) bool {
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
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
}
}

68
pkg/lambda/iterator.go Normal file
View File

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

View File

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

30
pkg/lambda/reduce.go Normal file
View File

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

View File

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

32
pkg/lambda/stringify.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
// Package "saccharine" provides a simple language built on top of λ-calculus,
// to facilitate productive coding using it.
package saccharine
import (
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
)
// Convert a piece of valid saccharine code into an expression.
func Parse(code string) (Expression, error) {
tokens, err := token.Parse(code)
if err != nil {
return nil, err
}
return parse(tokens)
}
// Convert a parsed saccharine expression back into source code.
func Stringify(expression Expression) string {
return stringifyExpression(expression)
}

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,10 @@
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
}
}
@@ -14,14 +12,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)
}
}
@@ -36,18 +34,8 @@ func (s Set[T]) ToList() []T {
return list
}
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]{}
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

8
tests/church_6^6.test Normal file
View File

@@ -0,0 +1,8 @@
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))

View File

@@ -1 +0,0 @@
VALUE

View File

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