From 31924237b26342d60856f20cd8575ed87997c1f4 Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Fri, 30 Jan 2026 20:16:57 -0500 Subject: [PATCH] feat: saccharine marshaler --- cmd/lambda/registry.go | 2 + pkg/saccharine/marshaler.go | 24 ++++++ pkg/saccharine/parse.go | 49 ++++++------ pkg/saccharine/saccharine.go | 22 ------ pkg/saccharine/{token/parse.go => scan.go} | 38 ++++----- pkg/saccharine/token.go | 91 ++++++++++++++++++++++ pkg/saccharine/token/token.go | 91 ---------------------- 7 files changed, 160 insertions(+), 157 deletions(-) create mode 100644 pkg/saccharine/marshaler.go delete mode 100644 pkg/saccharine/saccharine.go rename pkg/saccharine/{token/parse.go => scan.go} (70%) create mode 100644 pkg/saccharine/token.go delete mode 100644 pkg/saccharine/token/token.go diff --git a/cmd/lambda/registry.go b/cmd/lambda/registry.go index b983bc1..773afc0 100644 --- a/cmd/lambda/registry.go +++ b/cmd/lambda/registry.go @@ -6,6 +6,7 @@ import ( "git.maximhutz.com/max/lambda/pkg/convert" "git.maximhutz.com/max/lambda/pkg/engine/normalorder" "git.maximhutz.com/max/lambda/pkg/lambda" + "git.maximhutz.com/max/lambda/pkg/saccharine" ) func MakeRegistry() *registry.Registry { @@ -19,6 +20,7 @@ func MakeRegistry() *registry.Registry { // Marshalers r.AddMarshaler(cli.ConvertMarshaler(lambda.Marshaler{}, "lambda")) + r.AddMarshaler(cli.ConvertMarshaler(saccharine.Marshaler{}, "saccharine")) return r } diff --git a/pkg/saccharine/marshaler.go b/pkg/saccharine/marshaler.go new file mode 100644 index 0000000..bc5c3dc --- /dev/null +++ b/pkg/saccharine/marshaler.go @@ -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) diff --git a/pkg/saccharine/parse.go b/pkg/saccharine/parse.go index eb045d0..8b73394 100644 --- a/pkg/saccharine/parse.go +++ b/pkg/saccharine/parse.go @@ -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) diff --git a/pkg/saccharine/saccharine.go b/pkg/saccharine/saccharine.go deleted file mode 100644 index 57b1f17..0000000 --- a/pkg/saccharine/saccharine.go +++ /dev/null @@ -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) -} diff --git a/pkg/saccharine/token/parse.go b/pkg/saccharine/scan.go similarity index 70% rename from pkg/saccharine/token/parse.go rename to pkg/saccharine/scan.go index b7d5033..08e3459 100644 --- a/pkg/saccharine/token/parse.go +++ b/pkg/saccharine/scan.go @@ -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 { diff --git a/pkg/saccharine/token.go b/pkg/saccharine/token.go new file mode 100644 index 0000000..ee61dbf --- /dev/null +++ b/pkg/saccharine/token.go @@ -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() +} diff --git a/pkg/saccharine/token/token.go b/pkg/saccharine/token/token.go deleted file mode 100644 index 57771a5..0000000 --- a/pkg/saccharine/token/token.go +++ /dev/null @@ -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) -}