diff --git a/pkg/convert/saccharine_to_lambda.go b/pkg/convert/saccharine_to_lambda.go index 233bcde..bfaefa6 100644 --- a/pkg/convert/saccharine_to_lambda.go +++ b/pkg/convert/saccharine_to_lambda.go @@ -1 +1,108 @@ 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)) + } +} diff --git a/pkg/iterator/iterator.go b/pkg/iterator/iterator.go new file mode 100644 index 0000000..2db5cd4 --- /dev/null +++ b/pkg/iterator/iterator.go @@ -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 +} diff --git a/pkg/repr/lambda/expression.go b/pkg/repr/lambda/expression.go new file mode 100644 index 0000000..c27dfc1 --- /dev/null +++ b/pkg/repr/lambda/expression.go @@ -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} +} diff --git a/pkg/repr/lambda/generate_name.go b/pkg/repr/lambda/generate_name.go new file mode 100644 index 0000000..03f5018 --- /dev/null +++ b/pkg/repr/lambda/generate_name.go @@ -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 + } + } +} diff --git a/pkg/repr/lambda/get_free_variables.go b/pkg/repr/lambda/get_free_variables.go new file mode 100644 index 0000000..1c2bf91 --- /dev/null +++ b/pkg/repr/lambda/get_free_variables.go @@ -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 +} diff --git a/pkg/repr/lambda/is_free_variable.go b/pkg/repr/lambda/is_free_variable.go new file mode 100644 index 0000000..09d77a6 --- /dev/null +++ b/pkg/repr/lambda/is_free_variable.go @@ -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) +} diff --git a/pkg/repr/lambda/parse.go b/pkg/repr/lambda/parse.go new file mode 100644 index 0000000..25c3f11 --- /dev/null +++ b/pkg/repr/lambda/parse.go @@ -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") +} diff --git a/pkg/repr/lambda/rename.go b/pkg/repr/lambda/rename.go new file mode 100644 index 0000000..2c39237 --- /dev/null +++ b/pkg/repr/lambda/rename.go @@ -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) +} diff --git a/pkg/repr/lambda/substitute.go b/pkg/repr/lambda/substitute.go new file mode 100644 index 0000000..1f6b38e --- /dev/null +++ b/pkg/repr/lambda/substitute.go @@ -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) +} diff --git a/pkg/repr/repr.go b/pkg/repr/repr.go index 87961b5..fd671d7 100644 --- a/pkg/repr/repr.go +++ b/pkg/repr/repr.go @@ -1,10 +1,4 @@ package repr -import ( - "fmt" -) - type Repr interface { - fmt.Stringer - fmt.Scanner } diff --git a/pkg/repr/saccharine/expression.go b/pkg/repr/saccharine/expression.go new file mode 100644 index 0000000..7d830f5 --- /dev/null +++ b/pkg/repr/saccharine/expression.go @@ -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} +} diff --git a/pkg/repr/saccharine/lex.go b/pkg/repr/saccharine/lex.go new file mode 100644 index 0000000..5c78f5d --- /dev/null +++ b/pkg/repr/saccharine/lex.go @@ -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...) +} diff --git a/pkg/repr/saccharine/parse.go b/pkg/repr/saccharine/parse.go new file mode 100644 index 0000000..33aa63c --- /dev/null +++ b/pkg/repr/saccharine/parse.go @@ -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) +} diff --git a/pkg/repr/saccharine/statement.go b/pkg/repr/saccharine/statement.go new file mode 100644 index 0000000..cc5779d --- /dev/null +++ b/pkg/repr/saccharine/statement.go @@ -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} +} diff --git a/pkg/repr/saccharine/stringify.go b/pkg/repr/saccharine/stringify.go new file mode 100644 index 0000000..10d0393 --- /dev/null +++ b/pkg/repr/saccharine/stringify.go @@ -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)) + } +} diff --git a/pkg/repr/saccharine/token.go b/pkg/repr/saccharine/token.go new file mode 100644 index 0000000..dc5d48b --- /dev/null +++ b/pkg/repr/saccharine/token.go @@ -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) +} diff --git a/pkg/set/set.go b/pkg/set/set.go new file mode 100644 index 0000000..c66cf71 --- /dev/null +++ b/pkg/set/set.go @@ -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 +} diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go new file mode 100644 index 0000000..87a0337 --- /dev/null +++ b/pkg/trace/trace.go @@ -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) +}