refactor: rewrite CLI and internal architecture (#41)

## Description

The old architecture used a monolithic `main()` with a custom arg parser, an event-emitter-based runtime, and a plugin system for optional features.
This PR rewrites the CLI and internal architecture to be modular, extensible, and built around a registry of interchangeable components.

- Replace custom CLI arg parsing with Cobra subcommands (`convert`, `reduce`, `engine list`).
- Introduce a registry system (`internal/registry`) for marshalers, codecs, and engines, with BFS-based conversion path resolution.
- Add type-erased adapter layer (`internal/cli`) with `Repr`, `Engine`, `Process`, `Marshaler`, and `Conversion` interfaces wrapping generic `pkg/` types.
- Replace the event-emitter-based `Runtime` with a simpler `Engine`/`Process` model (`pkg/engine`).
- Add generic `Codec[T, U]` and `Marshaler[T]` interfaces (`pkg/codec`).
- Merge `saccharine/token` sub-package into `saccharine` and rename scanner functions from `parse*` to `scan*`.
- Make saccharine-to-lambda conversion bidirectional (encode and decode).
- Add `lambda.Marshaler` and `saccharine.Marshaler` implementing `codec.Marshaler`.
- Remove old infrastructure: `pkg/runtime`, `pkg/expr`, `internal/plugins`, `internal/statistics`.
- Add `make lint` target and update golangci-lint config.

### Decisions

- Cobra was chosen for the CLI framework to support nested subcommands and standard flag handling.
- The registry uses BFS to find conversion paths between representations, allowing multi-hop conversions without hardcoding routes.
- Type erasure via `cli.Repr` (wrapping `any`) enables the registry to work with heterogeneous types while keeping `pkg/` generics type-safe.
- The old plugin/event system was removed entirely rather than adapted, since the new `Process` model can support hooks differently in the future.

## Benefits

- Subcommands make the CLI self-documenting and easier to extend with new functionality.
- The registry pattern decouples representations, conversions, and engines, making it trivial to add new ones.
- BFS conversion routing means adding a single codec automatically enables transitive conversions.
- Simpler `Engine`/`Process` model reduces complexity compared to the event-emitter runtime.
- Consolidating the `token` sub-package reduces import depth and package sprawl.

## Checklist

- [x] Code follows conventional commit format.
- [x] Branch follows naming convention (`<type>/<description>`). Always use underscores.
- [ ] Tests pass (if applicable).
- [ ] Documentation updated (if applicable).

Reviewed-on: #41
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 #41.
This commit is contained in:
2026-02-07 03:25:32 +00:00
committed by Maxim Hutz
parent f2c8d9f7d2
commit a3ee34732e
41 changed files with 1007 additions and 637 deletions

View File

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