feat: copied code over
This commit is contained in:
@@ -1 +1,108 @@
|
|||||||
package convert
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/repr/lambda"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/repr/saccharine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertAtom(n *saccharine.Variable) lambda.Expression {
|
||||||
|
return lambda.NewVariable(n.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAbstraction(n *saccharine.Abstraction) lambda.Expression {
|
||||||
|
result := SaccharineToLambda(n.Body)
|
||||||
|
|
||||||
|
parameters := n.Parameters
|
||||||
|
|
||||||
|
// If the function has no parameters, it is a thunk. Lambda calculus still
|
||||||
|
// requires _some_ parameter exists, so generate one.
|
||||||
|
if len(parameters) == 0 {
|
||||||
|
freeVars := result.GetFree()
|
||||||
|
freshName := lambda.GenerateFreshName(freeVars)
|
||||||
|
parameters = append(parameters, freshName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(parameters) - 1; i >= 0; i-- {
|
||||||
|
result = lambda.NewAbstraction(parameters[i], result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertApplication(n *saccharine.Application) lambda.Expression {
|
||||||
|
result := SaccharineToLambda(n.Abstraction)
|
||||||
|
|
||||||
|
arguments := []lambda.Expression{}
|
||||||
|
for _, argument := range n.Arguments {
|
||||||
|
convertedArgument := SaccharineToLambda(argument)
|
||||||
|
arguments = append(arguments, convertedArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, argument := range arguments {
|
||||||
|
result = lambda.NewApplication(result, argument)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func reduceLet(s *saccharine.LetStatement, e lambda.Expression) lambda.Expression {
|
||||||
|
var value lambda.Expression
|
||||||
|
|
||||||
|
if len(s.Parameters) == 0 {
|
||||||
|
value = SaccharineToLambda(s.Body)
|
||||||
|
} else {
|
||||||
|
value = convertAbstraction(saccharine.NewAbstraction(s.Parameters, s.Body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lambda.NewApplication(
|
||||||
|
lambda.NewAbstraction(s.Name, e),
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reduceDeclare(s *saccharine.DeclareStatement, e lambda.Expression) lambda.Expression {
|
||||||
|
freshVar := lambda.GenerateFreshName(e.GetFree())
|
||||||
|
|
||||||
|
return lambda.NewApplication(
|
||||||
|
lambda.NewAbstraction(freshVar, e),
|
||||||
|
SaccharineToLambda(s.Value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reduceStatement(s saccharine.Statement, e lambda.Expression) lambda.Expression {
|
||||||
|
switch s := s.(type) {
|
||||||
|
case *saccharine.DeclareStatement:
|
||||||
|
return reduceDeclare(s, e)
|
||||||
|
case *saccharine.LetStatement:
|
||||||
|
return reduceLet(s, e)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown statement type: %v", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertClause(n *saccharine.Clause) lambda.Expression {
|
||||||
|
result := SaccharineToLambda(n.Returns)
|
||||||
|
|
||||||
|
for i := len(n.Statements) - 1; i >= 0; i-- {
|
||||||
|
result = reduceStatement(n.Statements[i], result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaccharineToLambda(n saccharine.Expression) lambda.Expression {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case *saccharine.Variable:
|
||||||
|
return convertAtom(n)
|
||||||
|
case *saccharine.Abstraction:
|
||||||
|
return convertAbstraction(n)
|
||||||
|
case *saccharine.Application:
|
||||||
|
return convertApplication(n)
|
||||||
|
case *saccharine.Clause:
|
||||||
|
return convertClause(n)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown expression type: %T", n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
86
pkg/iterator/iterator.go
Normal file
86
pkg/iterator/iterator.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
Package "iterator"
|
||||||
|
*/
|
||||||
|
package iterator
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// An iterator over slices.
|
||||||
|
type Iterator[T any] struct {
|
||||||
|
items []T
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new iterator, over a set of items.
|
||||||
|
func Of[T any](items []T) *Iterator[T] {
|
||||||
|
return &Iterator[T]{items: items, index: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the current position of the iterator.
|
||||||
|
func (i Iterator[T]) Index() int {
|
||||||
|
return i.index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Iterator[T]) Copy() *Iterator[T] {
|
||||||
|
return &Iterator[T]{items: i.items, index: i.index}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator[T]) Sync(o *Iterator[T]) {
|
||||||
|
i.index = o.index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new iterator, over a set of items.
|
||||||
|
func (i Iterator[T]) Get() (T, error) {
|
||||||
|
var null T
|
||||||
|
if i.Done() {
|
||||||
|
return null, fmt.Errorf("iterator is exhausted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.items[i.index], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Iterator[T]) MustGet() T {
|
||||||
|
var null T
|
||||||
|
if i.Done() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.items[i.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iterator[T]) Forward() {
|
||||||
|
if !i.Done() {
|
||||||
|
i.index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new iterator, over a set of items.
|
||||||
|
func (i *Iterator[T]) Next() (T, error) {
|
||||||
|
item, err := i.Get()
|
||||||
|
if err == nil {
|
||||||
|
i.index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new iterator, over a set of items.
|
||||||
|
func (i *Iterator[T]) Back() {
|
||||||
|
i.index = max(i.index-1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the current position of the iterator.
|
||||||
|
func (i Iterator[T]) Done() bool {
|
||||||
|
return i.index == len(i.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Do[T any, U any](i *Iterator[T], fn func(i *Iterator[T]) (U, error)) (U, error) {
|
||||||
|
i2 := i.Copy()
|
||||||
|
|
||||||
|
out, err := fn(i2)
|
||||||
|
if err == nil {
|
||||||
|
i.Sync(i2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
87
pkg/repr/lambda/expression.go
Normal file
87
pkg/repr/lambda/expression.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/repr"
|
||||||
|
"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 {
|
||||||
|
repr.Repr
|
||||||
|
|
||||||
|
// Substitute replaces all free occurrences of the target variable with the
|
||||||
|
// replacement expression. Alpha-renaming is performed automatically to
|
||||||
|
// avoid variable capture.
|
||||||
|
Substitute(target string, replacement Expression) Expression
|
||||||
|
|
||||||
|
// GetFree returns the set of all free variable names in the expression.
|
||||||
|
// This function does not mutate the input expression.
|
||||||
|
// The returned set is newly allocated and can be modified by the caller.
|
||||||
|
GetFree() set.Set[string]
|
||||||
|
|
||||||
|
// Rename replaces all occurrences of the target variable name with the new name.
|
||||||
|
Rename(target string, newName string) Expression
|
||||||
|
|
||||||
|
// IsFree returns true if the variable name n occurs free in the expression.
|
||||||
|
// This function does not mutate the input expression.
|
||||||
|
IsFree(n string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Expression = Abstraction{}
|
||||||
|
_ Expression = Application{}
|
||||||
|
_ Expression = Variable{}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type Abstraction struct {
|
||||||
|
parameter string
|
||||||
|
body Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Abstraction) Parameter() string {
|
||||||
|
return a.parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Abstraction) Body() Expression {
|
||||||
|
return a.body
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAbstraction(parameter string, body Expression) Abstraction {
|
||||||
|
return Abstraction{parameter, body}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
abstraction Expression
|
||||||
|
argument Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Application) Abstraction() Expression {
|
||||||
|
return a.abstraction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Application) Argument() Expression {
|
||||||
|
return a.argument
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplication(abstraction Expression, argument Expression) Application {
|
||||||
|
return Application{abstraction, argument}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type Variable struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Variable) Name() string {
|
||||||
|
return v.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVariable(name string) Variable {
|
||||||
|
return Variable{name}
|
||||||
|
}
|
||||||
19
pkg/repr/lambda/generate_name.go
Normal file
19
pkg/repr/lambda/generate_name.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateFreshName generates a variable name that is not in the used set.
|
||||||
|
// This function does not mutate the used set.
|
||||||
|
func GenerateFreshName(used set.Set[string]) string {
|
||||||
|
for i := uint64(0); ; i++ {
|
||||||
|
attempt := "_" + string(strconv.AppendUint(nil, i, 10))
|
||||||
|
|
||||||
|
if !used.Has(attempt) {
|
||||||
|
return attempt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
pkg/repr/lambda/get_free_variables.go
Normal file
19
pkg/repr/lambda/get_free_variables.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
import "git.maximhutz.com/max/lambda/pkg/set"
|
||||||
|
|
||||||
|
func (e Variable) GetFree() set.Set[string] {
|
||||||
|
return set.New(e.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Abstraction) GetFree() set.Set[string] {
|
||||||
|
vars := e.Body().GetFree()
|
||||||
|
vars.Remove(e.Parameter())
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Application) GetFree() set.Set[string] {
|
||||||
|
vars := e.Abstraction().GetFree()
|
||||||
|
vars.Merge(e.Argument().GetFree())
|
||||||
|
return vars
|
||||||
|
}
|
||||||
12
pkg/repr/lambda/is_free_variable.go
Normal file
12
pkg/repr/lambda/is_free_variable.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
func (e Variable) IsFree(n string) bool {
|
||||||
|
return e.Name() == n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Abstraction) IsFree(n string) bool {
|
||||||
|
return e.Parameter() != n && e.Body().IsFree(n)
|
||||||
|
}
|
||||||
|
func (e Application) IsFree(n string) bool {
|
||||||
|
return e.Abstraction().IsFree(n) || e.Argument().IsFree(n)
|
||||||
|
}
|
||||||
10
pkg/repr/lambda/parse.go
Normal file
10
pkg/repr/lambda/parse.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Application -> (x x)
|
||||||
|
// Abstraction -> \x.y
|
||||||
|
// Variable -> x
|
||||||
|
func Parse(code string) (Expression, error) {
|
||||||
|
return nil, fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
28
pkg/repr/lambda/rename.go
Normal file
28
pkg/repr/lambda/rename.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
// Rename replaces all occurrences of the target variable name with the new name.
|
||||||
|
func (e Variable) Rename(target string, newName string) Expression {
|
||||||
|
if e.Name() == target {
|
||||||
|
return NewVariable(newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Abstraction) Rename(target string, newName string) Expression {
|
||||||
|
newParam := e.Parameter()
|
||||||
|
if e.Parameter() == target {
|
||||||
|
newParam = newName
|
||||||
|
}
|
||||||
|
|
||||||
|
newBody := e.Body().Rename(target, newName)
|
||||||
|
|
||||||
|
return NewAbstraction(newParam, newBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Application) Rename(target string, newName string) Expression {
|
||||||
|
newAbs := e.Abstraction().Rename(target, newName)
|
||||||
|
newArg := e.Argument().Rename(target, newName)
|
||||||
|
|
||||||
|
return NewApplication(newAbs, newArg)
|
||||||
|
}
|
||||||
35
pkg/repr/lambda/substitute.go
Normal file
35
pkg/repr/lambda/substitute.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package lambda
|
||||||
|
|
||||||
|
func (e Variable) Substitute(target string, replacement Expression) Expression {
|
||||||
|
if e.Name() == target {
|
||||||
|
return replacement
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Abstraction) Substitute(target string, replacement Expression) Expression {
|
||||||
|
if e.Parameter() == target {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
body := e.Body()
|
||||||
|
param := e.Parameter()
|
||||||
|
if replacement.IsFree(param) {
|
||||||
|
freeVars := replacement.GetFree()
|
||||||
|
freeVars.Merge(body.GetFree())
|
||||||
|
freshVar := GenerateFreshName(freeVars)
|
||||||
|
body = body.Rename(param, freshVar)
|
||||||
|
param = freshVar
|
||||||
|
}
|
||||||
|
|
||||||
|
newBody := body.Substitute(target, replacement)
|
||||||
|
return NewAbstraction(param, newBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Application) Substitute(target string, replacement Expression) Expression {
|
||||||
|
abs := e.Abstraction().Substitute(target, replacement)
|
||||||
|
arg := e.Argument().Substitute(target, replacement)
|
||||||
|
|
||||||
|
return NewApplication(abs, arg)
|
||||||
|
}
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
package repr
|
package repr
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Repr interface {
|
type Repr interface {
|
||||||
fmt.Stringer
|
|
||||||
fmt.Scanner
|
|
||||||
}
|
}
|
||||||
|
|||||||
59
pkg/repr/saccharine/expression.go
Normal file
59
pkg/repr/saccharine/expression.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package saccharine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/repr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Expression interface {
|
||||||
|
repr.Repr
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Expression = Abstraction{}
|
||||||
|
_ Expression = Application{}
|
||||||
|
_ Expression = Variable{}
|
||||||
|
_ Expression = Clause{}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type Abstraction struct {
|
||||||
|
Parameters []string
|
||||||
|
Body Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAbstraction(parameter []string, body Expression) *Abstraction {
|
||||||
|
return &Abstraction{Parameters: parameter, Body: body}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
Abstraction Expression
|
||||||
|
Arguments []Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplication(abstraction Expression, arguments []Expression) *Application {
|
||||||
|
return &Application{Abstraction: abstraction, Arguments: arguments}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return &Clause{Statements: statements, Returns: returns}
|
||||||
|
}
|
||||||
130
pkg/repr/saccharine/lex.go
Normal file
130
pkg/repr/saccharine/lex.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package saccharine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/iterator"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isVariables determines whether a rune can be a valid variable.
|
||||||
|
func isVariable(r rune) bool {
|
||||||
|
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanRune(i *iterator.Iterator[rune], expected func(rune) bool) (rune, error) {
|
||||||
|
i2 := i.Copy()
|
||||||
|
|
||||||
|
if r, err := i2.Next(); err != nil {
|
||||||
|
return r, err
|
||||||
|
} else if !expected(r) {
|
||||||
|
return r, fmt.Errorf("got unexpected rune %v'", r)
|
||||||
|
} else {
|
||||||
|
i.Sync(i2)
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanCharacter(i *iterator.Iterator[rune], expected rune) (rune, error) {
|
||||||
|
i2 := i.Copy()
|
||||||
|
|
||||||
|
if r, err := i2.Next(); err != nil {
|
||||||
|
return r, err
|
||||||
|
} else if r != expected {
|
||||||
|
return r, fmt.Errorf("got unexpected rune %v'", r)
|
||||||
|
} else {
|
||||||
|
i.Sync(i2)
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 scanToken(i *iterator.Iterator[rune]) (*Token, error) {
|
||||||
|
index := i.Index()
|
||||||
|
|
||||||
|
if i.Done() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
letter, err := i.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, trace.Wrap(err, "cannot produce next token")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case letter == '(':
|
||||||
|
return NewOpenParen(index), nil
|
||||||
|
case letter == ')':
|
||||||
|
return NewCloseParen(index), nil
|
||||||
|
case letter == '.':
|
||||||
|
return NewDot(index), nil
|
||||||
|
case letter == '\\':
|
||||||
|
return NewSlash(index), nil
|
||||||
|
case letter == '\n':
|
||||||
|
return NewSoftBreak(index), nil
|
||||||
|
case letter == '{':
|
||||||
|
return NewOpenBrace(index), nil
|
||||||
|
case letter == '}':
|
||||||
|
return NewCloseBrace(index), nil
|
||||||
|
case letter == ':':
|
||||||
|
if _, err := scanCharacter(i, '='); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return NewAssign(index), nil
|
||||||
|
}
|
||||||
|
case letter == ';':
|
||||||
|
return NewHardBreak(index), nil
|
||||||
|
case letter == '#':
|
||||||
|
// Skip everything until the next newline or EOF.
|
||||||
|
for !i.Done() {
|
||||||
|
r, err := i.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, trace.Wrap(err, "error while parsing comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == '\n' {
|
||||||
|
// Put the newline back so it can be processed as a soft break.
|
||||||
|
i.Back()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
case unicode.IsSpace(letter):
|
||||||
|
return nil, nil
|
||||||
|
case isVariable(letter):
|
||||||
|
atom := []rune{letter}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if r, err := scanRune(i, isVariable); err != nil {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
atom = append(atom, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewAtom(string(atom), index), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown character '%v'", string(letter))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a string into tokens.
|
||||||
|
func Scan(input string) ([]Token, error) {
|
||||||
|
i := iterator.Of([]rune(input))
|
||||||
|
tokens := []Token{}
|
||||||
|
errorList := []error{}
|
||||||
|
|
||||||
|
for !i.Done() {
|
||||||
|
token, err := scanToken(i)
|
||||||
|
if err != nil {
|
||||||
|
errorList = append(errorList, err)
|
||||||
|
} else if token != nil {
|
||||||
|
tokens = append(tokens, *token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens, errors.Join(errorList...)
|
||||||
|
}
|
||||||
237
pkg/repr/saccharine/parse.go
Normal file
237
pkg/repr/saccharine/parse.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package saccharine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/iterator"
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenIterator = iterator.Iterator[Token]
|
||||||
|
|
||||||
|
func parseRawToken(i *TokenIterator, expected Type) (*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'", Name(expected), tok.Value)
|
||||||
|
} else {
|
||||||
|
return &tok, nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func passSoftBreaks(i *TokenIterator) {
|
||||||
|
for {
|
||||||
|
if _, err := parseRawToken(i, TokenSoftBreak); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToken(i *TokenIterator, expected Type, ignoreSoftBreaks bool) (*Token, error) {
|
||||||
|
return iterator.Do(i, func(i *TokenIterator) (*Token, error) {
|
||||||
|
if ignoreSoftBreaks {
|
||||||
|
passSoftBreaks(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseRawToken(i, expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseString(i *TokenIterator) (string, error) {
|
||||||
|
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, error) {
|
||||||
|
if tok, softErr := parseRawToken(i, TokenSoftBreak); softErr == nil {
|
||||||
|
return tok, nil
|
||||||
|
} else if tok, hardErr := parseRawToken(i, TokenHardBreak); hardErr == nil {
|
||||||
|
return tok, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.Join(softErr, hardErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseList[U any](i *TokenIterator, fn func(*TokenIterator) (U, error), minimum int) ([]U, error) {
|
||||||
|
results := []U{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if u, err := fn(i); err != nil {
|
||||||
|
if len(results) < minimum {
|
||||||
|
return nil, trace.Wrap(err, "expected at least '%v' items, got only '%v'", minimum, len(results))
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
} else {
|
||||||
|
results = append(results, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAbstraction(i *TokenIterator) (*Abstraction, error) {
|
||||||
|
return iterator.Do(i, func(i *TokenIterator) (*Abstraction, error) {
|
||||||
|
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, 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
|
||||||
|
} else {
|
||||||
|
return NewAbstraction(parameters, body), nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseApplication(i *TokenIterator) (*Application, error) {
|
||||||
|
return iterator.Do(i, func(i *TokenIterator) (*Application, error) {
|
||||||
|
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, 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAtom(i *TokenIterator) (*Variable, error) {
|
||||||
|
if tok, err := parseToken(i, TokenAtom, true); err != nil {
|
||||||
|
return nil, trace.Wrap(err, "no variable (col %d)", i.Index())
|
||||||
|
} else {
|
||||||
|
return NewVariable(tok.Value), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStatements(i *TokenIterator) ([]Statement, error) {
|
||||||
|
statements := []Statement{}
|
||||||
|
|
||||||
|
//nolint:errcheck
|
||||||
|
parseList(i, parseBreak, 0)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if statement, err := parseStatement(i); err != nil {
|
||||||
|
break
|
||||||
|
} else if _, err := parseList(i, parseBreak, 1); err != nil && !i.Done() {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
statements = append(statements, statement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statements, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseClause(i *TokenIterator, braces bool) (*Clause, error) {
|
||||||
|
if braces {
|
||||||
|
if _, err := parseToken(i, TokenOpenBrace, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stmts []Statement
|
||||||
|
var last *DeclareStatement
|
||||||
|
var err error
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if stmts, err = parseStatements(i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(stmts) == 0 {
|
||||||
|
return nil, fmt.Errorf("no statements in clause")
|
||||||
|
} else if last, ok = stmts[len(stmts)-1].(*DeclareStatement); !ok {
|
||||||
|
return nil, fmt.Errorf("this clause contains no final return value (col %d)", i.MustGet().Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
if braces {
|
||||||
|
if _, err := parseToken(i, TokenCloseBrace, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewClause(stmts[:len(stmts)-1], last.Value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExpression(i *TokenIterator) (Expression, error) {
|
||||||
|
return iterator.Do(i, func(i *TokenIterator) (Expression, error) {
|
||||||
|
passSoftBreaks(i)
|
||||||
|
|
||||||
|
switch peek := i.MustGet(); peek.Type {
|
||||||
|
case TokenOpenParen:
|
||||||
|
return parseApplication(i)
|
||||||
|
case TokenSlash:
|
||||||
|
return parseAbstraction(i)
|
||||||
|
case TokenAtom:
|
||||||
|
return parseAtom(i)
|
||||||
|
case TokenOpenBrace:
|
||||||
|
return parseClause(i, true)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("expected expression, got '%v' (col %d)", peek.Value, peek.Column)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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, TokenAssign, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if body, err := parseExpression(i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return NewLet(parameters[0], parameters[1:], body), nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDeclare(i *TokenIterator) (*DeclareStatement, error) {
|
||||||
|
if value, err := parseExpression(i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return NewDeclare(value), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStatement(i *TokenIterator) (Statement, error) {
|
||||||
|
if let, letErr := parseLet(i); letErr == nil {
|
||||||
|
return let, nil
|
||||||
|
} else if declare, declErr := parseDeclare(i); declErr == nil {
|
||||||
|
return declare, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.Join(letErr, declErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a list of tokens, attempt to parse it into an syntax tree.
|
||||||
|
func parse(tokens []Token) (Expression, error) {
|
||||||
|
i := iterator.Of(tokens)
|
||||||
|
|
||||||
|
exp, err := parseClause(i, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !i.Done() {
|
||||||
|
return nil, fmt.Errorf("expected EOF, found more code (col %d)", i.MustGet().Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
31
pkg/repr/saccharine/statement.go
Normal file
31
pkg/repr/saccharine/statement.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package saccharine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximhutz.com/max/lambda/pkg/repr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Statement interface {
|
||||||
|
repr.Repr
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type LetStatement struct {
|
||||||
|
Name string
|
||||||
|
Parameters []string
|
||||||
|
Body Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLet(name string, parameters []string, body Expression) *LetStatement {
|
||||||
|
return &LetStatement{Name: name, Parameters: parameters, Body: body}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
type DeclareStatement struct {
|
||||||
|
Value Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeclare(value Expression) *DeclareStatement {
|
||||||
|
return &DeclareStatement{Value: value}
|
||||||
|
}
|
||||||
69
pkg/repr/saccharine/stringify.go
Normal file
69
pkg/repr/saccharine/stringify.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package saccharine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func stringifyAtom(n *Variable) string {
|
||||||
|
return n.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyAbstraction(n *Abstraction) string {
|
||||||
|
return "\\" + strings.Join(n.Parameters, " ") + "." + Stringify(n.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyApplication(n *Application) string {
|
||||||
|
arguments := []string{Stringify(n.Abstraction)}
|
||||||
|
|
||||||
|
for _, argument := range n.Arguments {
|
||||||
|
arguments = append(arguments, Stringify(argument))
|
||||||
|
}
|
||||||
|
|
||||||
|
return "(" + strings.Join(arguments, " ") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyLet(s *LetStatement) string {
|
||||||
|
return s.Name + " " + strings.Join(s.Parameters, " ") + " := " + Stringify(s.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyDeclare(s *DeclareStatement) string {
|
||||||
|
return Stringify(s.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyStatement(s Statement) string {
|
||||||
|
switch s := s.(type) {
|
||||||
|
case *DeclareStatement:
|
||||||
|
return stringifyDeclare(s)
|
||||||
|
case *LetStatement:
|
||||||
|
return stringifyLet(s)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown statement type: %v", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyClause(n *Clause) string {
|
||||||
|
stmts := ""
|
||||||
|
|
||||||
|
for _, statement := range n.Statements {
|
||||||
|
stmts += stringifyStatement(statement) + "; "
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{ " + stmts + Stringify(n.Returns) + " }"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert an expression back into valid source code.
|
||||||
|
func Stringify(n Expression) string {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case *Variable:
|
||||||
|
return stringifyAtom(n)
|
||||||
|
case *Abstraction:
|
||||||
|
return stringifyAbstraction(n)
|
||||||
|
case *Application:
|
||||||
|
return stringifyApplication(n)
|
||||||
|
case *Clause:
|
||||||
|
return stringifyClause(n)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown expression type: %T", n))
|
||||||
|
}
|
||||||
|
}
|
||||||
91
pkg/repr/saccharine/token.go
Normal file
91
pkg/repr/saccharine/token.go
Normal 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)
|
||||||
|
}
|
||||||
57
pkg/set/set.go
Normal file
57
pkg/set/set.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import "iter"
|
||||||
|
|
||||||
|
type Set[T comparable] map[T]bool
|
||||||
|
|
||||||
|
func (s Set[T]) Add(items ...T) {
|
||||||
|
for _, item := range items {
|
||||||
|
s[item] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Has(item T) bool {
|
||||||
|
return s[item]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Remove(items ...T) {
|
||||||
|
for _, item := range items {
|
||||||
|
delete(s, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Merge(o Set[T]) {
|
||||||
|
for item := range o {
|
||||||
|
s.Add(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) ToList() []T {
|
||||||
|
list := []T{}
|
||||||
|
|
||||||
|
for item := range s {
|
||||||
|
list = append(list, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Items() iter.Seq[T] {
|
||||||
|
return func(yield func(T) bool) {
|
||||||
|
for item := range s {
|
||||||
|
if !yield(item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New[T comparable](items ...T) Set[T] {
|
||||||
|
result := Set[T]{}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
result.Add(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
25
pkg/trace/trace.go
Normal file
25
pkg/trace/trace.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package trace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Indent(s string, size int) string {
|
||||||
|
lines := strings.Lines(s)
|
||||||
|
indent := strings.Repeat(" ", size)
|
||||||
|
|
||||||
|
indented := ""
|
||||||
|
for line := range lines {
|
||||||
|
indented += indent + line
|
||||||
|
}
|
||||||
|
|
||||||
|
return indented
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(child error, format string, a ...any) error {
|
||||||
|
parent := fmt.Errorf(format, a...)
|
||||||
|
childErrString := Indent(child.Error(), 4)
|
||||||
|
return errors.New(parent.Error() + "\n" + childErrString)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user