3 Commits

Author SHA1 Message Date
30e3bfe8f8 style: scanner not lexer 2026-01-19 19:40:55 -05:00
091bc70172 feat: copied code over 2026-01-19 19:32:08 -05:00
5f2dcc9394 feat: progress 2026-01-18 18:27:48 -05:00
48 changed files with 417 additions and 1037 deletions

View File

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

View File

@@ -1,53 +1,30 @@
BINARY_NAME=lambda BINARY_NAME=lambda
TEST=simple
.PHONY: help build run profile explain graph docs test bench lint clean .PHONY: help build docs test bench clean
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.SILENT: .SILENT:
help: help:
echo "Available targets:" echo "Available targets:"
echo " build - Build the lambda executable" echo " build - Build the lambda executable"
echo " run - Build and run the lambda 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 " docs - Start local godoc server on port 6060"
echo " test - Run tests for all samples" echo " test - Run tests"
echo " bench - Run benchmarks for all samples" echo " bench - Run benchmarks"
echo " lint - Run golangci-lint on all packages"
echo " clean - Remove all build artifacts" echo " clean - Remove all build artifacts"
build: build:
go build -o ${BINARY_NAME} ./cmd/lambda go build -o ${BINARY_NAME} ./cmd/lambda
chmod +x ${BINARY_NAME} chmod +x ${BINARY_NAME}
run: build
./${BINARY_NAME} -s -f ./tests/$(TEST).test -o program.out
profile: build
./${BINARY_NAME} -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out
explain: build
./${BINARY_NAME} -x -p profile/cpu.prof -f ./tests/$(TEST).test -o program.out > explain.out
graph:
go tool pprof -raw -output=profile/cpu.raw profile/cpu.prof
go tool pprof -svg profile/cpu.prof > profile/cpu.svg
echo ">>> View at 'file://$(PWD)/profile/cpu.svg'"
docs: docs:
echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'" echo ">>> View at 'http://localhost:6060/pkg/git.maximhutz.com/max/lambda/'"
go run golang.org/x/tools/cmd/godoc@latest -http=:6060 go run golang.org/x/tools/cmd/godoc@latest -http=:6060
test: test:
go test -v ./cmd/lambda go test -v ./...
bench: bench:
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda go test -bench=. -benchtime=10x -cpu=4 ./...
lint:
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
clean: clean:
rm -f ${BINARY_NAME} rm -f ${BINARY_NAME}

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

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

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

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

View File

@@ -1,67 +0,0 @@
package main
import (
"os"
"git.maximhutz.com/max/lambda/internal/cli"
"git.maximhutz.com/max/lambda/internal/config"
"git.maximhutz.com/max/lambda/internal/plugins"
"git.maximhutz.com/max/lambda/pkg/convert"
"git.maximhutz.com/max/lambda/pkg/normalorder"
"git.maximhutz.com/max/lambda/pkg/saccharine"
)
func main() {
// Parse CLI arguments.
options, err := config.FromArgs()
cli.HandleError(err)
logger := options.GetLogger()
logger.Info("using program arguments", "args", os.Args)
logger.Info("parsed CLI options", "options", options)
// Get input.
input, err := options.Source.Extract()
cli.HandleError(err)
// Parse code into syntax tree.
ast, err := saccharine.Parse(input)
cli.HandleError(err)
logger.Info("parsed syntax tree", "tree", ast)
// Compile expression to lambda calculus.
compiled := convert.SaccharineToLambda(ast)
logger.Info("compiled λ expression", "tree", compiled.String())
// Create reducer with the compiled expression.
runtime := normalorder.NewRuntime(compiled)
// If the user selected to track CPU performance, attach a profiler.
if options.Profile != "" {
plugins.NewPerformance(options.Profile, runtime)
}
// 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 := runtime.Expression().String()
err = options.Destination.Write(result)
cli.HandleError(err)
}

View File

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

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
}

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

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

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

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

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

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

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

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

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

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

3
go.mod
View File

@@ -6,6 +6,9 @@ require github.com/stretchr/testify v1.11.1
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

9
go.sum
View File

@@ -1,9 +1,18 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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

View File

@@ -1,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} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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

@@ -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,17 +3,16 @@ package convert
import ( import (
"fmt" "fmt"
"git.maximhutz.com/max/lambda/pkg/codec" "git.maximhutz.com/max/lambda/pkg/repr/lambda"
"git.maximhutz.com/max/lambda/pkg/lambda" "git.maximhutz.com/max/lambda/pkg/repr/saccharine"
"git.maximhutz.com/max/lambda/pkg/saccharine"
) )
func encodeAtom(n *saccharine.Atom) lambda.Expression { func convertAtom(n *saccharine.Variable) lambda.Expression {
return lambda.NewVariable(n.Name) return lambda.NewVariable(n.Name)
} }
func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression { func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
result := encodeExpression(n.Body) result := SaccharineToLambda(n.Body)
parameters := n.Parameters parameters := n.Parameters
@@ -32,13 +31,13 @@ func encodeAbstraction(n *saccharine.Abstraction) lambda.Expression {
return result return result
} }
func encodeApplication(n *saccharine.Application) lambda.Expression { func convertApplication(n *saccharine.Application) lambda.Expression {
result := encodeExpression(n.Abstraction) result := SaccharineToLambda(n.Abstraction)
arguments := []lambda.Expression{} arguments := []lambda.Expression{}
for _, argument := range n.Arguments { for _, argument := range n.Arguments {
encodeedArgument := encodeExpression(argument) convertedArgument := SaccharineToLambda(argument)
arguments = append(arguments, encodeedArgument) arguments = append(arguments, convertedArgument)
} }
for _, argument := range arguments { for _, argument := range arguments {
@@ -52,9 +51,9 @@ func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expressio
var value lambda.Expression var value lambda.Expression
if len(s.Parameters) == 0 { if len(s.Parameters) == 0 {
value = encodeExpression(s.Body) value = SaccharineToLambda(s.Body)
} else { } else {
value = encodeAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body)) value = convertAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
} }
return lambda.NewApplication( return lambda.NewApplication(
@@ -68,7 +67,7 @@ func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.E
return lambda.NewApplication( return lambda.NewApplication(
lambda.NewAbstraction(freshVar, e), 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 { func convertClause(n *saccharine.Clause) lambda.Expression {
result := encodeExpression(n.Returns) result := SaccharineToLambda(n.Returns)
for i := len(n.Statements) - 1; i >= 0; i-- { for i := len(n.Statements) - 1; i >= 0; i-- {
result = reduceStatement(n.Statements[i], result) result = reduceStatement(n.Statements[i], result)
@@ -93,46 +92,17 @@ func encodeClause(n *saccharine.Clause) lambda.Expression {
return result return result
} }
func encodeExpression(s saccharine.Expression) lambda.Expression { func SaccharineToLambda(n saccharine.Expression) lambda.Expression {
switch s := s.(type) { switch n := n.(type) {
case *saccharine.Atom: case *saccharine.Variable:
return encodeAtom(s) return convertAtom(n)
case *saccharine.Abstraction: case *saccharine.Abstraction:
return encodeAbstraction(s) return convertAbstraction(n)
case *saccharine.Application: case *saccharine.Application:
return encodeApplication(s) return convertApplication(n)
case *saccharine.Clause: case *saccharine.Clause:
return encodeClause(s) return convertClause(n)
default: 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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,14 @@
package lambda package lambda
import ( import (
"fmt" "git.maximhutz.com/max/lambda/pkg/repr"
"git.maximhutz.com/max/lambda/pkg/set" "git.maximhutz.com/max/lambda/pkg/set"
) )
// Expression is the interface for all lambda calculus expression types. // Expression is the interface for all lambda calculus expression types.
// It embeds the general expr.Expression interface for cross-mode compatibility. // It embeds the general expr.Expression interface for cross-mode compatibility.
type Expression interface { type Expression interface {
fmt.Stringer repr.Repr
// Substitute replaces all free occurrences of the target variable with the // Substitute replaces all free occurrences of the target variable with the
// replacement expression. Alpha-renaming is performed automatically to // replacement expression. Alpha-renaming is performed automatically to
@@ -29,6 +28,12 @@ type Expression interface {
IsFree(n string) bool IsFree(n string) bool
} }
var (
_ Expression = Abstraction{}
_ Expression = Application{}
_ Expression = Variable{}
)
/** ------------------------------------------------------------------------- */ /** ------------------------------------------------------------------------- */
type Abstraction struct { type Abstraction struct {
@@ -36,8 +41,6 @@ type Abstraction struct {
body Expression body Expression
} }
var _ Expression = Abstraction{}
func (a Abstraction) Parameter() string { func (a Abstraction) Parameter() string {
return a.parameter return a.parameter
} }
@@ -46,10 +49,6 @@ func (a Abstraction) Body() Expression {
return a.body return a.body
} }
func (a Abstraction) String() string {
return "\\" + a.parameter + "." + a.body.String()
}
func NewAbstraction(parameter string, body Expression) Abstraction { func NewAbstraction(parameter string, body Expression) Abstraction {
return Abstraction{parameter, body} return Abstraction{parameter, body}
} }
@@ -61,8 +60,6 @@ type Application struct {
argument Expression argument Expression
} }
var _ Expression = Application{}
func (a Application) Abstraction() Expression { func (a Application) Abstraction() Expression {
return a.abstraction return a.abstraction
} }
@@ -71,10 +68,6 @@ func (a Application) Argument() Expression {
return a.argument return a.argument
} }
func (a Application) String() string {
return "(" + a.abstraction.String() + " " + a.argument.String() + ")"
}
func NewApplication(abstraction Expression, argument Expression) Application { func NewApplication(abstraction Expression, argument Expression) Application {
return Application{abstraction, argument} return Application{abstraction, argument}
} }
@@ -85,16 +78,10 @@ type Variable struct {
name string name string
} }
var _ Expression = Variable{}
func (v Variable) Name() string { func (v Variable) Name() string {
return v.name return v.name
} }
func (v Variable) String() string {
return v.name
}
func NewVariable(name string) Variable { func NewVariable(name string) Variable {
return Variable{name} return Variable{name}
} }

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

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

View File

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

View File

@@ -10,12 +10,12 @@ import (
type TokenIterator = iterator.Iterator[Token] type TokenIterator = iterator.Iterator[Token]
func parseRawToken(i *TokenIterator, expected TokenType) (*Token, error) { func parseRawToken(i *TokenIterator, expected Type) (*Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*Token, error) { return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
if tok, err := i.Next(); err != nil { if tok, err := i.Next(); err != nil {
return nil, err return nil, err
} else if tok.Type != expected { } else if tok.Type != expected {
return nil, fmt.Errorf("expected token %v, got %v'", expected.Name(), tok.Value) return nil, fmt.Errorf("expected token %v, got %v'", Name(expected), tok.Value)
} else { } else {
return &tok, nil return &tok, nil
} }
@@ -30,7 +30,7 @@ func passSoftBreaks(i *TokenIterator) {
} }
} }
func parseToken(i *TokenIterator, expected TokenType, ignoreSoftBreaks bool) (*Token, error) { func parseToken(i *TokenIterator, expected Type, ignoreSoftBreaks bool) (*Token, error) {
return iterator.Do(i, func(i *TokenIterator) (*Token, error) { return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
if ignoreSoftBreaks { if ignoreSoftBreaks {
passSoftBreaks(i) passSoftBreaks(i)
@@ -103,11 +103,11 @@ func parseApplication(i *TokenIterator) (*Application, error) {
}) })
} }
func parseAtom(i *TokenIterator) (*Atom, error) { func parseAtom(i *TokenIterator) (*Variable, error) {
if tok, err := parseToken(i, TokenAtom, true); err != nil { if tok, err := parseToken(i, TokenAtom, true); err != nil {
return nil, trace.Wrap(err, "no variable (col %d)", i.Index()) return nil, trace.Wrap(err, "no variable (col %d)", i.Index())
} else { } else {
return NewAtom(tok.Value), nil return NewVariable(tok.Value), nil
} }
} }
@@ -225,3 +225,13 @@ func parse(tokens []Token) (Expression, error) {
return exp, nil return exp, nil
} }
// Convert a piece of valid saccharine code into an expression.
func Parse(code string) (Expression, error) {
tokens, err := Scan(code)
if err != nil {
return nil, err
}
return parse(tokens)
}

View File

@@ -56,27 +56,27 @@ func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
switch { switch {
case letter == '(': case letter == '(':
return NewTokenOpenParen(index), nil return NewOpenParen(index), nil
case letter == ')': case letter == ')':
return NewTokenCloseParen(index), nil return NewCloseParen(index), nil
case letter == '.': case letter == '.':
return NewTokenDot(index), nil return NewDot(index), nil
case letter == '\\': case letter == '\\':
return NewTokenSlash(index), nil return NewSlash(index), nil
case letter == '\n': case letter == '\n':
return NewTokenSoftBreak(index), nil return NewSoftBreak(index), nil
case letter == '{': case letter == '{':
return NewTokenOpenBrace(index), nil return NewOpenBrace(index), nil
case letter == '}': case letter == '}':
return NewTokenCloseBrace(index), nil return NewCloseBrace(index), nil
case letter == ':': case letter == ':':
if _, err := scanCharacter(i, '='); err != nil { if _, err := scanCharacter(i, '='); err != nil {
return nil, err return nil, err
} else { } else {
return NewTokenAssign(index), nil return NewAssign(index), nil
} }
case letter == ';': case letter == ';':
return NewTokenHardBreak(index), nil return NewHardBreak(index), nil
case letter == '#': case letter == '#':
// Skip everything until the next newline or EOF. // Skip everything until the next newline or EOF.
for !i.Done() { for !i.Done() {
@@ -105,14 +105,14 @@ func scanToken(i *iterator.Iterator[rune]) (*Token, error) {
} }
} }
return NewTokenAtom(string(atom), index), nil return NewAtom(string(atom), index), nil
} }
return nil, fmt.Errorf("unknown character '%v'", string(letter)) return nil, fmt.Errorf("unknown character '%v'", string(letter))
} }
// scan a string into tokens. // Parse a string into tokens.
func scan(input string) ([]Token, error) { func Scan(input string) ([]Token, error) {
i := iterator.Of([]rune(input)) i := iterator.Of([]rune(input))
tokens := []Token{} tokens := []Token{}
errorList := []error{} errorList := []error{}

View File

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

View File

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

View File

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

View File

@@ -1,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

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