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:
8
pkg/codec/codec.go
Normal file
8
pkg/codec/codec.go
Normal 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]
|
||||
@@ -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)
|
||||
|
||||
11
pkg/engine/engine.go
Normal file
11
pkg/engine/engine.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package engine
|
||||
|
||||
type Engine[T any] interface {
|
||||
Load() Process[T]
|
||||
}
|
||||
|
||||
type Process[T any] interface {
|
||||
Get() (T, error)
|
||||
Set(T) error
|
||||
Step(int) bool
|
||||
}
|
||||
42
pkg/engine/normalorder/engine.go
Normal file
42
pkg/engine/normalorder/engine.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package normalorder
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/engine"
|
||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||
)
|
||||
|
||||
type Process struct {
|
||||
expr lambda.Expression
|
||||
}
|
||||
|
||||
func (e Process) Get() (lambda.Expression, error) {
|
||||
return e.expr, nil
|
||||
}
|
||||
|
||||
func (e *Process) Set(l lambda.Expression) error {
|
||||
e.expr = l
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Process) Step(i int) bool {
|
||||
for range i {
|
||||
next, reduced := ReduceOnce(e.expr)
|
||||
if !reduced {
|
||||
return false
|
||||
}
|
||||
|
||||
e.expr = next
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
}
|
||||
|
||||
func (e Engine) Load() engine.Process[lambda.Expression] {
|
||||
return &Process{}
|
||||
}
|
||||
|
||||
var _ engine.Process[lambda.Expression] = (*Process)(nil)
|
||||
var _ engine.Engine[lambda.Expression] = (*Engine)(nil)
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
package lambda
|
||||
|
||||
import (
|
||||
"git.maximhutz.com/max/lambda/pkg/expr"
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/set"
|
||||
)
|
||||
|
||||
// Expression is the interface for all lambda calculus expression types.
|
||||
// It embeds the general expr.Expression interface for cross-mode compatibility.
|
||||
type Expression interface {
|
||||
expr.Expression
|
||||
fmt.Stringer
|
||||
|
||||
// Substitute replaces all free occurrences of the target variable with the
|
||||
// replacement expression. Alpha-renaming is performed automatically to
|
||||
|
||||
19
pkg/lambda/marshaler.go
Normal file
19
pkg/lambda/marshaler.go
Normal 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)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
24
pkg/saccharine/marshaler.go
Normal file
24
pkg/saccharine/marshaler.go
Normal 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)
|
||||
@@ -5,18 +5,17 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.maximhutz.com/max/lambda/pkg/iterator"
|
||||
"git.maximhutz.com/max/lambda/pkg/saccharine/token"
|
||||
"git.maximhutz.com/max/lambda/pkg/trace"
|
||||
)
|
||||
|
||||
type TokenIterator = iterator.Iterator[token.Token]
|
||||
type TokenIterator = iterator.Iterator[Token]
|
||||
|
||||
func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
|
||||
func parseRawToken(i *TokenIterator, expected TokenType) (*Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
||||
if tok, err := i.Next(); err != nil {
|
||||
return nil, err
|
||||
} else if tok.Type != expected {
|
||||
return nil, fmt.Errorf("expected token %v, got %v'", token.Name(expected), tok.Value)
|
||||
return nil, fmt.Errorf("expected token %v, got %v'", expected.Name(), tok.Value)
|
||||
} else {
|
||||
return &tok, nil
|
||||
}
|
||||
@@ -25,14 +24,14 @@ func parseRawToken(i *TokenIterator, expected token.Type) (*token.Token, error)
|
||||
|
||||
func passSoftBreaks(i *TokenIterator) {
|
||||
for {
|
||||
if _, err := parseRawToken(i, token.SoftBreak); err != nil {
|
||||
if _, err := parseRawToken(i, TokenSoftBreak); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*token.Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*token.Token, error) {
|
||||
func parseToken(i *TokenIterator, expected TokenType, ignoreSoftBreaks bool) (*Token, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
||||
if ignoreSoftBreaks {
|
||||
passSoftBreaks(i)
|
||||
}
|
||||
@@ -42,17 +41,17 @@ func parseToken(i *TokenIterator, expected token.Type, ignoreSoftBreaks bool) (*
|
||||
}
|
||||
|
||||
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())
|
||||
} else {
|
||||
return tok.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseBreak(i *TokenIterator) (*token.Token, error) {
|
||||
if tok, softErr := parseRawToken(i, token.SoftBreak); softErr == nil {
|
||||
func parseBreak(i *TokenIterator) (*Token, error) {
|
||||
if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == 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
|
||||
} else {
|
||||
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) {
|
||||
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)
|
||||
} else if parameters, err := parseList(i, parseString, 0); err != nil {
|
||||
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)
|
||||
} else if body, err := parseExpression(i); err != nil {
|
||||
return nil, err
|
||||
@@ -92,11 +91,11 @@ func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
||||
|
||||
func parseApplication(i *TokenIterator) (*Application, error) {
|
||||
return iterator.Do(i, func(i *TokenIterator) (*Application, error) {
|
||||
if _, err := parseToken(i, 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)
|
||||
} else if expressions, err := parseList(i, parseExpression, 1); err != nil {
|
||||
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)
|
||||
} else {
|
||||
return NewApplication(expressions[0], expressions[1:]), nil
|
||||
@@ -105,7 +104,7 @@ func parseApplication(i *TokenIterator) (*Application, 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())
|
||||
} else {
|
||||
return NewAtom(tok.Value), nil
|
||||
@@ -133,7 +132,7 @@ func parseStatements(i *TokenIterator) ([]Statement, error) {
|
||||
|
||||
func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
||||
if braces {
|
||||
if _, err := parseToken(i, token.OpenBrace, true); err != nil {
|
||||
if _, err := parseToken(i, TokenOpenBrace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -152,7 +151,7 @@ func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
||||
}
|
||||
|
||||
if braces {
|
||||
if _, err := parseToken(i, token.CloseBrace, true); err != nil {
|
||||
if _, err := parseToken(i, TokenCloseBrace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -165,13 +164,13 @@ func parseExpression(i *TokenIterator) (Expression, error) {
|
||||
passSoftBreaks(i)
|
||||
|
||||
switch peek := i.MustGet(); peek.Type {
|
||||
case token.OpenParen:
|
||||
case TokenOpenParen:
|
||||
return parseApplication(i)
|
||||
case token.Slash:
|
||||
case TokenSlash:
|
||||
return parseAbstraction(i)
|
||||
case token.Atom:
|
||||
case TokenAtom:
|
||||
return parseAtom(i)
|
||||
case token.OpenBrace:
|
||||
case TokenOpenBrace:
|
||||
return parseClause(i, true)
|
||||
default:
|
||||
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) {
|
||||
if parameters, err := parseList(i, parseString, 1); err != nil {
|
||||
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
|
||||
} else if body, err := parseExpression(i); err != nil {
|
||||
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.
|
||||
func parse(tokens []token.Token) (Expression, error) {
|
||||
func parse(tokens []Token) (Expression, error) {
|
||||
i := iterator.Of(tokens)
|
||||
|
||||
exp, err := parseClause(i, false)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package token
|
||||
package saccharine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -14,7 +14,7 @@ func isVariable(r rune) bool {
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
// 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()
|
||||
|
||||
if i.Done() {
|
||||
@@ -56,27 +56,27 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
|
||||
switch {
|
||||
case letter == '(':
|
||||
return NewOpenParen(index), nil
|
||||
return NewTokenOpenParen(index), nil
|
||||
case letter == ')':
|
||||
return NewCloseParen(index), nil
|
||||
return NewTokenCloseParen(index), nil
|
||||
case letter == '.':
|
||||
return NewDot(index), nil
|
||||
return NewTokenDot(index), nil
|
||||
case letter == '\\':
|
||||
return NewSlash(index), nil
|
||||
return NewTokenSlash(index), nil
|
||||
case letter == '\n':
|
||||
return NewSoftBreak(index), nil
|
||||
return NewTokenSoftBreak(index), nil
|
||||
case letter == '{':
|
||||
return NewOpenBrace(index), nil
|
||||
return NewTokenOpenBrace(index), nil
|
||||
case letter == '}':
|
||||
return NewCloseBrace(index), nil
|
||||
return NewTokenCloseBrace(index), nil
|
||||
case letter == ':':
|
||||
if _, err := parseCharacter(i, '='); err != nil {
|
||||
if _, err := scanCharacter(i, '='); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return NewAssign(index), nil
|
||||
return NewTokenAssign(index), nil
|
||||
}
|
||||
case letter == ';':
|
||||
return NewHardBreak(index), nil
|
||||
return NewTokenHardBreak(index), nil
|
||||
case letter == '#':
|
||||
// Skip everything until the next newline or EOF.
|
||||
for !i.Done() {
|
||||
@@ -98,27 +98,27 @@ func getToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||
atom := []rune{letter}
|
||||
|
||||
for {
|
||||
if r, err := parseRune(i, isVariable); err != nil {
|
||||
if r, err := scanRune(i, isVariable); err != nil {
|
||||
break
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
|
||||
// Parse a string into tokens.
|
||||
func Parse(input string) ([]Token, error) {
|
||||
// scan a string into tokens.
|
||||
func scan(input string) ([]Token, error) {
|
||||
i := iterator.Of([]rune(input))
|
||||
tokens := []Token{}
|
||||
errorList := []error{}
|
||||
|
||||
for !i.Done() {
|
||||
token, err := getToken(i)
|
||||
token, err := scanToken(i)
|
||||
if err != nil {
|
||||
errorList = append(errorList, err)
|
||||
} else if token != nil {
|
||||
91
pkg/saccharine/token.go
Normal file
91
pkg/saccharine/token.go
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user