4 Commits

Author SHA1 Message Date
31924237b2 feat: saccharine marshaler 2026-01-30 20:16:57 -05:00
68cc1624c7 feat: registry, normalorder engine, lambda codec and marshaler 2026-01-30 20:07:35 -05:00
0cdce0e42c feat: cli versions 2026-01-30 17:54:47 -05:00
0ec52008bb feat: repr, codec 2026-01-30 16:24:17 -05:00
30 changed files with 555 additions and 466 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,6 +126,9 @@ linters:
# Blank import should be only in a main or test package, or have a comment justifying it. # Blank import should be only in a main or test package, or have a comment justifying it.
- name: blank-imports - name: blank-imports
# Packages should have comments of the form "Package x ...".
- name: package-comments
# context.Context() should be the first parameter of a function when provided as argument. # context.Context() should be the first parameter of a function when provided as argument.
- name: context-as-argument - name: context-as-argument
arguments: arguments:

View File

@@ -1,7 +1,7 @@
BINARY_NAME=lambda BINARY_NAME=lambda
TEST=simple TEST=simple
.PHONY: help build run profile explain graph docs test bench clean .PHONY: help build run profile explain graph docs test bench lint clean
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.SILENT: .SILENT:
@@ -15,6 +15,7 @@ help:
echo " docs - Start local godoc server on port 6060" echo " docs - Start local godoc server on port 6060"
echo " test - Run tests for all samples" echo " test - Run tests for all samples"
echo " bench - Run benchmarks for all samples" echo " bench - Run benchmarks for all samples"
echo " lint - Run golangci-lint on all packages"
echo " clean - Remove all build artifacts" echo " clean - Remove all build artifacts"
build: build:
@@ -45,6 +46,9 @@ test:
bench: bench:
go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda go test -bench=. -benchtime=10x -cpu=4 ./cmd/lambda
lint:
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
clean: clean:
rm -f ${BINARY_NAME} rm -f ${BINARY_NAME}
rm -f program.out rm -f program.out

26
cmd/lambda/registry.go Normal file
View File

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

55
internal/cli/codec.go Normal file
View File

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

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

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

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

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

21
internal/cli/repr.go Normal file
View File

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

View File

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

View File

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

View File

@@ -1,59 +0,0 @@
// Package "performance" provides a tracker to observer CPU performance during
// execution.
package plugins
import (
"os"
"path/filepath"
"runtime/pprof"
"git.maximhutz.com/max/lambda/pkg/runtime"
)
// Observes a reduction process, and publishes a CPU performance profile on
// completion.
type Performance struct {
File string
filePointer *os.File
Error error
}
// Create a performance tracker that outputs a profile to "file".
func NewPerformance(file string, process runtime.Runtime) *Performance {
plugin := &Performance{File: file}
process.On(runtime.StartEvent, plugin.Start)
process.On(runtime.StopEvent, plugin.Stop)
return plugin
}
// Begin profiling.
func (t *Performance) Start() {
var absPath string
absPath, t.Error = filepath.Abs(t.File)
if t.Error != nil {
return
}
t.Error = os.MkdirAll(filepath.Dir(absPath), 0777)
if t.Error != nil {
return
}
t.filePointer, t.Error = os.Create(absPath)
if t.Error != nil {
return
}
t.Error = pprof.StartCPUProfile(t.filePointer)
if t.Error != nil {
return
}
}
// Stop profiling.
func (t *Performance) Stop() {
pprof.StopCPUProfile()
t.filePointer.Close()
}

View File

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

View File

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

View File

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

8
pkg/codec/codec.go Normal file
View File

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

View File

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

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

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

View File

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

View File

@@ -1,15 +0,0 @@
// Package expr provides the abstract Expression interface for all evaluatable
// expression types in the lambda runtime.
package expr
import (
"fmt"
)
// Expression is the base interface for all evaluatable expression types.
// Different evaluation modes (lambda calculus, SKI combinators, typed lambda
// calculus, etc.) implement this interface with their own concrete types.
type Expression interface {
// The expression should have a human-readable representation.
fmt.Stringer
}

View File

@@ -1,14 +1,15 @@
package lambda package lambda
import ( import (
"git.maximhutz.com/max/lambda/pkg/expr" "fmt"
"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 {
expr.Expression fmt.Stringer
// 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

19
pkg/lambda/marshaler.go Normal file
View File

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

View File

@@ -1,46 +0,0 @@
package normalorder
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
"git.maximhutz.com/max/lambda/pkg/lambda"
"git.maximhutz.com/max/lambda/pkg/runtime"
)
// NormalOrderReducer implements normal order (leftmost-outermost) reduction
// for lambda calculus expressions.
type Runtime struct {
emitter.BaseEmitter[runtime.Event]
expression lambda.Expression
}
// NewNormalOrderReducer creates a new normal order reducer.
func NewRuntime(expression lambda.Expression) *Runtime {
return &Runtime{
BaseEmitter: *emitter.New[runtime.Event](),
expression: expression,
}
}
// Expression returns the current expression state.
func (r *Runtime) Expression() expr.Expression {
return r.expression
}
func (r *Runtime) Step() bool {
result, done := ReduceOnce(r.expression)
r.expression = result
return !done
}
// Reduce performs normal order reduction on a lambda expression.
// The expression must be a lambda.Expression; other types are returned unchanged.
func (r *Runtime) Run() {
r.Emit(runtime.StartEvent)
for !r.Step() {
r.Emit(runtime.StepEvent)
}
r.Emit(runtime.StopEvent)
}

View File

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

View File

@@ -1,27 +0,0 @@
// Package runtime provides the abstract Reducer interface for all expression
// reduction strategies.
package runtime
import (
"git.maximhutz.com/max/lambda/pkg/emitter"
"git.maximhutz.com/max/lambda/pkg/expr"
)
// Runtime defines the interface for expression reduction strategies.
// Different evaluation modes (normal order, applicative order, SKI combinators,
// etc.) implement this interface with their own reduction logic.
//
// Runtimes also implement the Emitter interface to allow plugins to observe
// reduction lifecycle events (Start, Step, Stop).
type Runtime interface {
emitter.Emitter[Event]
// Run a single step. Returns whether the runtime is complete or not.
Step() bool
// Run until completion.
Run()
// Copy the state of the runtime.
Expression() expr.Expression
}

View File

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

View File

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

View File

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

View File

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

91
pkg/saccharine/token.go Normal file
View File

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

View File

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