docs: document remaining packages and simplify AST types (#45)

## Summary

- Added doc comments across the codebase: `pkg/lambda`, `pkg/saccharine`, `pkg/codec`, `pkg/engine`, `pkg/iterator`, `pkg/set`, `pkg/convert`, `internal/registry`, and `cmd/lambda`.
- Made lambda and saccharine expression structs use public fields instead of getters, matching `go/ast` conventions.
- Removed superfluous constructors for saccharine and lambda expression/statement types in favor of struct literals.
- Consolidated saccharine token constructors into a single `NewToken` function.
- Removed the unused `trace` package.

## Test plan

- [x] `go build ./...` passes.
- [x] `go test ./...` passes.
- [ ] Verify `go doc` output renders correctly for documented packages.

Reviewed-on: #45
Co-authored-by: M.V. Hutz <git@maximhutz.me>
Co-committed-by: M.V. Hutz <git@maximhutz.me>
This commit was merged in pull request #45.
This commit is contained in:
2026-02-10 01:15:41 +00:00
committed by Maxim Hutz
parent 1f486875fd
commit 361f529bdc
33 changed files with 506 additions and 463 deletions

View File

@@ -7,18 +7,24 @@ import (
"git.maximhutz.com/max/lambda/pkg/codec"
)
// A Codec is a type-erased codec that serializes and deserializes expressions
// as Expr values, regardless of the underlying representation type.
type Codec interface {
codec.Codec[Expr]
// InType returns the name of the representation this codec handles.
InType() string
}
type convertedCodec[T any] struct {
// A registeredCodec adapts a typed codec.Codec[T] into the type-erased Codec
// interface. It wraps decoded values into Expr on decode, and extracts the
// underlying T from an Expr on encode.
type registeredCodec[T any] struct {
codec codec.Codec[T]
inType string
}
func (c convertedCodec[T]) Decode(s string) (Expr, error) {
func (c registeredCodec[T]) Decode(s string) (Expr, error) {
t, err := c.codec.Decode(s)
if err != nil {
return nil, err
@@ -27,7 +33,7 @@ func (c convertedCodec[T]) Decode(s string) (Expr, error) {
return NewExpr(c.inType, t), nil
}
func (c convertedCodec[T]) Encode(r Expr) (string, error) {
func (c registeredCodec[T]) Encode(r Expr) (string, error) {
t, ok := r.Data().(T)
if !ok {
dataType := reflect.TypeOf(r.Data())
@@ -38,13 +44,15 @@ func (c convertedCodec[T]) Encode(r Expr) (string, error) {
return c.codec.Encode(t)
}
func (c convertedCodec[T]) InType() string { return c.inType }
func (c registeredCodec[T]) InType() string { return c.inType }
// RegisterCodec registers a typed codec under the given representation name.
// Returns an error if a codec for that representation is already registered.
func RegisterCodec[T any](registry *Registry, m codec.Codec[T], inType string) error {
if _, ok := registry.codecs[inType]; ok {
return fmt.Errorf("Codec for '%s' already registered", inType)
}
registry.codecs[inType] = convertedCodec[T]{m, inType}
registry.codecs[inType] = registeredCodec[T]{m, inType}
return nil
}

View File

@@ -6,19 +6,29 @@ import (
"git.maximhutz.com/max/lambda/pkg/codec"
)
// A Conversion is a type-erased transformation from one representation to
// another. It operates on Expr values, hiding the underlying representation
// types.
type Conversion interface {
// InType returns the name of the source representation.
InType() string
// OutType returns the name of the target representation.
OutType() string
// Run applies the conversion to the given expression. Returns an error if
// the expression's data does not match the expected source type.
Run(Expr) (Expr, error)
}
type convertedConversion[T, U any] struct {
// A registeredConversion adapts a typed codec.Conversion[T, U] into the
// type-erased Conversion interface. It extracts the underlying T from an Expr,
// applies the conversion, and wraps the result as a new Expr.
type registeredConversion[T, U any] struct {
conversion codec.Conversion[T, U]
inType, outType string
}
func (c convertedConversion[T, U]) Run(expr Expr) (Expr, error) {
func (c registeredConversion[T, U]) Run(expr Expr) (Expr, error) {
t, ok := expr.Data().(T)
if !ok {
return nil, fmt.Errorf("could not parse '%v' as '%s'", t, c.inType)
@@ -32,12 +42,18 @@ func (c convertedConversion[T, U]) Run(expr Expr) (Expr, error) {
return NewExpr(c.outType, u), nil
}
func (c convertedConversion[T, U]) InType() string { return c.inType }
func (c registeredConversion[T, U]) InType() string { return c.inType }
func (c convertedConversion[T, U]) OutType() string { return c.outType }
func (c registeredConversion[T, U]) OutType() string { return c.outType }
func RegisterConversion[T, U any](registry *Registry, conversion func(T) (U, error), inType, outType string) error {
registry.converter.Add(convertedConversion[T, U]{conversion, inType, outType})
// RegisterConversion registers a typed conversion function between two
// representations.
func RegisterConversion[T, U any](
registry *Registry,
conversion codec.Conversion[T, U],
inType, outType string,
) error {
registry.converter.Add(registeredConversion[T, U]{conversion, inType, outType})
return nil
}

View File

@@ -1,13 +1,18 @@
package registry
// A Converter is a directed graph of conversions between representations. Each
// node is a representation name, and each edge is a Conversion.
type Converter struct {
data map[string][]Conversion
}
// NewConverter creates an empty Converter with no registered conversions.
func NewConverter() *Converter {
return &Converter{data: map[string][]Conversion{}}
}
// Add registers a conversion, adding an edge from its source representation
// to its target representation.
func (g *Converter) Add(c Conversion) {
conversionsFromIn, ok := g.data[c.InType()]
if !ok {
@@ -18,6 +23,8 @@ func (g *Converter) Add(c Conversion) {
g.data[c.InType()] = conversionsFromIn
}
// ConversionsFrom returns all conversions that have the given representation
// as their source type.
func (g *Converter) ConversionsFrom(t string) []Conversion {
return g.data[t]
}

View File

@@ -6,26 +6,36 @@ import (
"git.maximhutz.com/max/lambda/pkg/engine"
)
// An Engine is a type-erased evaluation engine that can load an expression
// into a runnable Process.
type Engine interface {
// Load prepares an expression for evaluation, returning a Process. Returns
// an error if the expression's data does not match the engine's expected
// representation type.
Load(Expr) (Process, error)
// Name returns the name of this engine.
Name() string
// InType returns the name of the representation this engine operates on.
InType() string
}
type convertedEngine[T any] struct {
// A registeredEngine adapts a typed engine.Engine[T] into the type-erased
// Engine interface. It extracts the underlying T from an Expr before passing it
// to the engine.
type registeredEngine[T any] struct {
engine engine.Engine[T]
name string
inType string
}
func (e convertedEngine[T]) InType() string { return e.inType }
func (e registeredEngine[T]) InType() string { return e.inType }
func (e convertedEngine[T]) Name() string { return e.name }
func (e registeredEngine[T]) Name() string { return e.name }
func (e convertedEngine[T]) Load(expr Expr) (Process, error) {
func (e registeredEngine[T]) Load(expr Expr) (Process, error) {
t, ok := expr.Data().(T)
if !ok {
return nil, fmt.Errorf("'ncorrent format '%s' for engine '%s'", expr.Repr(), e.inType)
return nil, fmt.Errorf("incorrect format '%s' for engine '%s'", expr.Repr(), e.inType)
}
process, err := e.engine(t)
@@ -33,14 +43,16 @@ func (e convertedEngine[T]) Load(expr Expr) (Process, error) {
return nil, err
}
return convertedProcess[T]{process, e.inType}, nil
return registeredProcess[T]{process, e.inType}, nil
}
// RegisterEngine registers a typed engine under the given name. Returns an
// error if an engine with that name is already registered.
func RegisterEngine[T any](registry *Registry, e engine.Engine[T], name, inType string) error {
if _, ok := registry.engines[name]; ok {
return fmt.Errorf("engine '%s' already registered", name)
}
registry.engines[name] = &convertedEngine[T]{e, name, inType}
registry.engines[name] = &registeredEngine[T]{e, name, inType}
return nil
}

View File

@@ -1,24 +1,26 @@
package registry
// A Expr is a lambda calculus expression. It can have any type of
// Expresentation, so long as that class is known to the registry it is handled
// An Expr is a type-erased lambda calculus expression. It can have any type of
// representation, so long as that type is known to the registry it is handled
// by.
type Expr interface {
// Repr returns the name of the underlying Expresentation. It is assumed if
// two expressions have the same Repr(), they have the same Expresentation.
// Repr returns the name of the underlying representation. Two expressions
// with the same Repr() are assumed to have the same representation type.
Repr() string
// The base expression data.
// Data returns the underlying expression data.
Data() any
}
// A baseExpr is the default implementation of Expr.
type baseExpr struct {
id string
data any
}
func (r baseExpr) Repr() string { return r.id }
func (e baseExpr) Repr() string { return e.id }
func (r baseExpr) Data() any { return r.data }
func (e baseExpr) Data() any { return e.data }
// NewExpr creates an Expr with the given representation name and data.
func NewExpr(id string, data any) Expr { return baseExpr{id, data} }

View File

@@ -4,28 +4,32 @@ import (
"git.maximhutz.com/max/lambda/pkg/engine"
)
// A Process is a type-erased reduction process that operates on Expr values.
type Process interface {
engine.Process[Expr]
// InType returns the name of the representation this process operates on.
InType() string
}
type convertedProcess[T any] struct {
// A registeredProcess adapts a typed engine.Process[T] into the type-erased
// Process interface. It wraps the result of Get into an Expr.
type registeredProcess[T any] struct {
process engine.Process[T]
inType string
}
func (e convertedProcess[T]) InType() string { return e.inType }
func (p registeredProcess[T]) InType() string { return p.inType }
func (b convertedProcess[T]) Get() (Expr, error) {
s, err := b.process.Get()
func (p registeredProcess[T]) Get() (Expr, error) {
s, err := p.process.Get()
if err != nil {
return nil, err
}
return NewExpr(b.inType, s), nil
return NewExpr(p.inType, s), nil
}
func (b convertedProcess[T]) Step(i int) bool {
return b.process.Step(i)
func (p registeredProcess[T]) Step(i int) bool {
return p.process.Step(i)
}

View File

@@ -1,3 +1,5 @@
// Package registry defines a structure to hold all available representations,
// engines, and conversions between them.
package registry
import (
@@ -6,12 +8,15 @@ import (
"maps"
)
// A Registry holds all representations, conversions, codecs, and engines
// available to the program.
type Registry struct {
codecs map[string]Codec
converter *Converter
engines map[string]Engine
}
// New makes an empty registry.
func New() *Registry {
return &Registry{
codecs: map[string]Codec{},
@@ -20,6 +25,8 @@ func New() *Registry {
}
}
// GetEngine finds an engine based on its name. Returns an error if an engine
// with that name cannot be found.
func (r Registry) GetEngine(name string) (Engine, error) {
e, ok := r.engines[name]
if !ok {
@@ -29,10 +36,13 @@ func (r Registry) GetEngine(name string) (Engine, error) {
return e, nil
}
// ListEngines returns all available engines to the registry.
func (r Registry) ListEngines() iter.Seq[Engine] {
return maps.Values(r.engines)
}
// GetDefaultEngine infers the preferred engine for a representation. Returns an
// error if one cannot be chosen.
func (r *Registry) GetDefaultEngine(id string) (Engine, error) {
for _, engine := range r.engines {
if engine.InType() == id {
@@ -45,6 +55,12 @@ func (r *Registry) GetDefaultEngine(id string) (Engine, error) {
// return nil, fmt.Errorf("no engine for '%s'", id)
}
// ConvertTo attempts to convert an expression of one type of representation to
// another. Returns the converted expression, otherwise an error.
//
// It can convert between any two types of representations, given there is a
// valid conversion path between them. It uses BFS to traverse a graph of
// conversion edges, and converts along the shortest path.
func (r *Registry) ConvertTo(expr Expr, outType string) (Expr, error) {
path, err := r.ConversionPath(expr.Repr(), outType)
if err != nil {
@@ -62,6 +78,8 @@ func (r *Registry) ConvertTo(expr Expr, outType string) (Expr, error) {
return result, err
}
// Marshal serializes an expression, given that representation has a codec.
// Returns an error if the representation is not registered, or it has no codec.
func (r *Registry) Marshal(expr Expr) (string, error) {
m, ok := r.codecs[expr.Repr()]
if !ok {
@@ -71,6 +89,8 @@ func (r *Registry) Marshal(expr Expr) (string, error) {
return m.Encode(expr)
}
// Unmarshal deserializes an expression. Returns an error if the representation
// or a codec for it is not registered.
func (r *Registry) Unmarshal(s string, outType string) (Expr, error) {
m, ok := r.codecs[outType]
if !ok {
@@ -94,6 +114,9 @@ func reverse[T any](list []T) []T {
return reversed
}
// ConversionPath attempts to find a set of valid conversions that (if applied)
// convert one representation to another. Returns an error if no path can be
// found.
func (r *Registry) ConversionPath(from, to string) ([]Conversion, error) {
backtrack := map[string]Conversion{}
iteration := []string{from}