Documentation
¶
Overview ¶
Package sexpr provides tools for working with S-expressions.
Entry points ¶
There are three, layered on each other:
- Tokenize scans a reader into an iter.Seq2 of Token values, keeping the source text of each lexeme. Use it when you want the lexemes and their positions without an AST.
- Parse builds a File from a reader, turning tokens into Node values and decoding string escapes and number literals along the way.
- Print writes a File back out as S-expression text. Parsing what Print produced yields the same values it was given.
Grammar ¶
A source is any number of top level datums. A datum is one of:
symbol add, x, +, ->list, hello-world number 42, -7, 0.5, -1.5e-3 string "hello", "a\nb", "é" boolean #t, #true, #f, #false nil nil list (), (add 1 2), (a b . c) quoted 'x, `x, ,x, ,@x
A symbol is a run of letters, digits, and the punctuation "+-*/<>=!?:$%_&~^@." which is not a number and is not the single character ".". Letters include non-ASCII ones, so π is a symbol.
A number is an optional sign, then digits with an optional fraction and an optional decimal exponent. Digits are ASCII only. A decimal point must have a digit after it, so "1." is not a number, and an exponent must have at least one digit. A lexeme which begins like a number but is malformed, such as "12abc" or "--1", is an error rather than a symbol. Parse yields an Int for a literal with neither fraction nor exponent and a Float otherwise.
A string is delimited by double quotes and may contain the escapes \" \\ \n \r \t \b \f and \uXXXX, where X is a hex digit. Any other escape is an error. Tokenize keeps the escapes as written; Parse decodes them into String.Value.
A list is a parenthesized sequence of datums. Writing a "." before the last datum makes an improper list, whose tail is held in List.Tail rather than in List.Elements. The empty list "()" is distinct from Nil.
The reader macros "'", "`", ",", and ",@" each apply to the datum which follows, producing a Quote. The shorthand is kept rather than rewritten to (quote x), so printing reproduces what was written.
Comments come in two forms: ";" runs to the end of the line, and "#| ... |#" spans lines and nests. Both are kept, attached to the File or List which encloses them.
Nesting is bounded by MaxDepth, which Parse enforces so a pathological input fails with a MaxDepthExceededError rather than exhausting the stack.
Out of scope ¶
This package covers the common core of S-expression syntax, not any one Lisp's full reader. None of the following is supported, and each is a syntax error rather than a silently accepted extension:
- "[...]" bracket lists
- "#;" datum comments
- "#\char" character literals
- "#(...)" vectors
- "|escaped symbols|"
- the full numeric tower: rationals, complex numbers, arbitrary precision integers, and the "#x", "#o", "#b", "#e", and "#i" prefixes. Numbers are decimal, and are an int64 or a float64.
- the canonical and base64 transport encodings of the S-expression internet draft
Marshalling between S-expressions and Go structs is also out of scope. This package gives you the syntax tree; mapping it onto your own types is yours to do.
Printing layout ¶
Print writes each top level datum on its own line.
A list is written on one line, with its elements separated by a single space, whenever that line would end at or before MaxLineWidth:
(add 1 2)
When it would run past that, the list breaks onto one element per line. The head stays beside the opening parenthesis and the remaining elements are indented two spaces past it, so they line up under the head:
(function-with-a-long-name argument-one argument-two)
The indent is measured from the list's own opening parenthesis, so nesting compounds it: a list which starts at column 2 indents its elements to 4. Only the lists which do not fit are broken, so a short inner list stays on one line inside a broken outer one.
The tail of a dotted pair follows the same rule, written on its own line after the dot:
(aaa bbb . ccc)
Quote shorthands are reproduced as written, never expanded to (quote x), and a broken datum after one is indented past the macro.
An atom is never broken, so a string or symbol longer than MaxLineWidth simply runs past it.
Comments ¶
Comments are written back in source order among the datums of whichever container holds them, each on its own line:
; leading (add 1 2) ; trailing
A line comment is always followed by a newline, so it can never swallow the code after it. A block comment is written exactly as it appeared, delimiters included.
Because of that, a list holding any comment is always broken across lines even when it would otherwise fit, and a comment written before a list's first element pushes that element off the opening line:
( ; about a a b)
A comment after the last element likewise moves the closing parenthesis onto its own line.
Index ¶
- Constants
- Variables
- func Print(w io.Writer, f *File) error
- func Tokenize(r io.Reader) iter.Seq2[Token, error]
- type Bool
- type Comment
- type File
- type Float
- type Int
- type InvalidEscapeError
- type InvalidNumberError
- type InvalidSymbolError
- type List
- type MaxDepthExceededError
- type Nil
- type Node
- type NonFiniteFloatError
- type NumberRangeError
- type Pos
- type Quote
- type QuoteKind
- type String
- type Symbol
- type TailWithoutElementsError
- type Token
- type TokenType
- type UnexpectedCharacterError
- type UnexpectedEndOfTokensError
- type UnexpectedTokenError
- type UnsupportedNodeError
- type UnterminatedCommentError
- type UnterminatedStringError
Examples ¶
Constants ¶
const MaxDepth = 10_000
MaxDepth is the deepest list nesting Parse accepts.
Parsing is recursive descent, so nesting costs stack. Rather than let a pathological input exhaust it, anything deeper than this fails with a MaxDepthExceededError.
const MaxLineWidth = 80
MaxLineWidth is the column past which Print breaks a list across lines.
Width is counted in bytes, matching how Pos counts columns.
Variables ¶
var ErrNilComment = errors.New("cannot print a nil comment")
ErrNilComment is returned by Print when a container holds a nil Comment.
var ErrNilFile = errors.New("cannot print a nil file")
Functions ¶
func Print ¶
Print the given File to the given writer as S-expression text.
Example ¶
package main
import (
"fmt"
"os"
"github.com/z5labs/sexpr-go"
)
func main() {
f := &sexpr.File{
Nodes: []sexpr.Node{
sexpr.List{
Elements: []sexpr.Node{
sexpr.Symbol{Value: "add"},
sexpr.Int{Value: 1},
sexpr.Int{Value: 2},
},
},
sexpr.List{
Elements: []sexpr.Node{
sexpr.Symbol{Value: "define"},
sexpr.Symbol{Value: "a-name-long-enough-to-need-breaking"},
sexpr.String{Value: "a value long enough to push past the line width"},
},
},
},
}
if err := sexpr.Print(os.Stdout, f); err != nil {
fmt.Println(err)
}
}
Output: (add 1 2) (define a-name-long-enough-to-need-breaking "a value long enough to push past the line width")
func Tokenize ¶
Tokenize the S-expression defined in the given reader.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/z5labs/sexpr-go"
)
func main() {
src := `(add 1 2.5 "hi" #t) ; done`
for tok, err := range sexpr.Tokenize(strings.NewReader(src)) {
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%d:%d %s\n", tok.Pos.Line, tok.Pos.Column, tok)
}
}
Output: 1:1 LParen(() 1:2 Symbol(add) 1:6 Number(1) 1:8 Number(2.5) 1:12 String(hi) 1:17 Bool(#t) 1:19 RParen()) 1:21 Comment(; done)
Types ¶
type File ¶
File represents a parsed S-expression source, which may hold any number of top level datums.
func Parse ¶
Parse the S-expressions defined in the given reader.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/z5labs/sexpr-go"
)
func main() {
src := `(add 1 2.5 "hi" #t nil)`
f, err := sexpr.Parse(strings.NewReader(src))
if err != nil {
fmt.Println(err)
return
}
list := f.Nodes[0].(sexpr.List)
for _, elem := range list.Elements {
switch n := elem.(type) {
case sexpr.Symbol:
fmt.Printf("symbol %s\n", n.Value)
case sexpr.Int:
fmt.Printf("int %d\n", n.Value)
case sexpr.Float:
fmt.Printf("float %v\n", n.Value)
case sexpr.String:
fmt.Printf("string %q\n", n.Value)
case sexpr.Bool:
fmt.Printf("bool %t\n", n.Value)
case sexpr.Nil:
fmt.Println("nil")
}
}
}
Output: symbol add int 1 float 2.5 string "hi" bool true nil
Example (Errors) ¶
package main
import (
"fmt"
"strings"
"github.com/z5labs/sexpr-go"
)
func main() {
_, err := sexpr.Parse(strings.NewReader(`(add 1`))
if err != nil {
fmt.Println(err)
}
_, err = sexpr.Parse(strings.NewReader(`(add 1 . )`))
if err != nil {
fmt.Println(err)
}
_, err = sexpr.Parse(strings.NewReader(`(add 12abc)`))
if err != nil {
fmt.Println(err)
}
_, err = sexpr.Parse(strings.NewReader(`(add "oops)`))
if err != nil {
fmt.Println(err)
}
}
Output: unexpected end of tokens at line 1, column 6, expected one of: RParen unexpected token at line 1, column 10: RParen()), expected one of: LParen, Quote, Symbol, String, Number, Bool invalid number literal "12abc" at line 1, column 6 unterminated string literal at line 1, column 6
type InvalidEscapeError ¶
InvalidEscapeError is the error returned by the tokenizer when a string literal contains an unrecognized escape sequence.
func (InvalidEscapeError) Error ¶
func (e InvalidEscapeError) Error() string
Error implements the [error] interface.
type InvalidNumberError ¶
InvalidNumberError is the error returned by the tokenizer when a lexeme is meant to be a number but is malformed.
func (InvalidNumberError) Error ¶
func (e InvalidNumberError) Error() string
Error implements the [error] interface.
type InvalidSymbolError ¶
type InvalidSymbolError struct {
// Value comes first because the message leads with it, and because being
// field-identical to [Symbol] is a coincidence worth not relying on.
Value string
Pos Pos
}
InvalidSymbolError is the error returned by the printer when a Symbol holds text which is not a symbol.
Symbols are written verbatim because the format has no way to quote them, so a value such as "123" or "a b" would read back as something other than the symbol it came from.
func (InvalidSymbolError) Error ¶
func (e InvalidSymbolError) Error() string
Error implements the [error] interface.
type List ¶
type List struct {
Pos Pos
Elements []Node
Tail Node
// Comments holds the comments written inside this list, in the order they
// appear. They are attached to the list rather than to any one element.
Comments []*Comment
}
List represents a parenthesized list. An empty list has no elements and is distinct from Nil.
Tail holds the datum after the dot of an improper list such as (a . b), and is nil for a proper list. Elements always holds what precedes the dot, so (a b . c) has two elements and a tail.
type MaxDepthExceededError ¶
MaxDepthExceededError is the error returned by the parser when the input nests deeper than MaxDepth.
func (MaxDepthExceededError) Error ¶
func (e MaxDepthExceededError) Error() string
Error implements the [error] interface.
type Node ¶
type Node interface {
// contains filtered or unexported methods
}
Node represents a node in the S-expression AST.
The interface is sealed by an unexported method, so only this package can implement it and a type switch over the node types is exhaustive.
type NonFiniteFloatError ¶
type NonFiniteFloatError struct {
Value float64
}
NonFiniteFloatError is the error returned by the printer when a Float holds an infinity or a NaN.
S-expressions have no syntax for those, so writing one would produce output which reads back as a symbol rather than a number.
func (NonFiniteFloatError) Error ¶
func (e NonFiniteFloatError) Error() string
Error implements the [error] interface.
type NumberRangeError ¶
NumberRangeError is the error returned by the parser when a number literal is well formed but cannot be represented as an int64 or a float64.
func (NumberRangeError) Error ¶
func (e NumberRangeError) Error() string
Error implements the [error] interface.
type Pos ¶
Pos represents the position of a token in the input.
Line is 1-based. Column is a 1-based byte offset into the line, so a multi-byte rune advances Column by its encoded width rather than by one.
type Quote ¶
Quote represents a datum written with one of the reader macro shorthands.
The shorthand is kept rather than rewritten to (quote x), so that printing reproduces what was written.
type QuoteKind ¶
type QuoteKind int
QuoteKind identifies which reader macro produced a Quote.
const ( QuoteKindQuote QuoteKind = iota // ' QuoteKindQuasiquote // ` QuoteKindUnquote // , QuoteKindUnquoteSplicing // ,@ )
The reader macro shorthands a Quote may carry.
func (QuoteKind) String ¶
String implements the fmt.Stringer interface. It panics on a QuoteKind which is not one of the constants above, since that can only come from a value this package never produced.
type String ¶
String represents a string literal. Value holds the decoded text, with the escape sequences of the source resolved.
type TailWithoutElementsError ¶
type TailWithoutElementsError struct {
Pos Pos
}
TailWithoutElementsError is the error returned by the printer when a List carries a tail but has no elements.
A dot separates two halves, so there is no S-expression for a tail with nothing before it; writing "(. b)" would produce text the parser rejects.
func (TailWithoutElementsError) Error ¶
func (e TailWithoutElementsError) Error() string
Error implements the [error] interface.
type Token ¶
Token represents a token in an S-expression.
func (Token) String ¶
String implements the fmt.Stringer interface, rendering the token as its type followed by its value in parentheses, such as Symbol(add).
type TokenType ¶
type TokenType int
TokenType represents the type of a token.
const ( TokenLParen TokenType = iota // "(" TokenRParen // ")" TokenSymbol // e.g. add, x, +, ->list TokenComment // e.g. ; comment or #| comment |# TokenString // e.g. "hello" TokenNumber // e.g. 42, -0.5, 1.5e-3 TokenDot // "." separating the halves of a dotted pair TokenBool // #t, #true, #f, or #false TokenQuote // a reader macro: ', `, , or ,@ )
The token types Tokenize may yield. Punctuation gets its own type rather than being lumped together, since TokenSymbol means the Lisp atom here.
func (TokenType) String ¶
String implements the fmt.Stringer interface. It panics on a TokenType which is not one of the constants above, since that can only come from a value this package never produced.
type UnexpectedCharacterError ¶
UnexpectedCharacterError is the error returned by the tokenizer when it encounters an unexpected character in the input.
func (UnexpectedCharacterError) Error ¶
func (e UnexpectedCharacterError) Error() string
Error implements the [error] interface.
type UnexpectedEndOfTokensError ¶
UnexpectedEndOfTokensError is the error returned by the parser when it reaches the end of the tokens unexpectedly.
func (UnexpectedEndOfTokensError) Error ¶
func (e UnexpectedEndOfTokensError) Error() string
Error implements the [error] interface.
type UnexpectedTokenError ¶
UnexpectedTokenError is the error returned by the parser when it encounters an unexpected token.
func (UnexpectedTokenError) Error ¶
func (e UnexpectedTokenError) Error() string
Error implements the [error] interface.
type UnsupportedNodeError ¶
type UnsupportedNodeError struct {
Node Node
}
UnsupportedNodeError is the error returned by the printer when it meets a node it cannot write.
func (UnsupportedNodeError) Error ¶
func (e UnsupportedNodeError) Error() string
Error implements the [error] interface.
type UnterminatedCommentError ¶
type UnterminatedCommentError struct {
Pos Pos
}
UnterminatedCommentError is the error returned by the tokenizer when a block comment is never closed.
func (UnterminatedCommentError) Error ¶
func (e UnterminatedCommentError) Error() string
Error implements the [error] interface.
type UnterminatedStringError ¶
type UnterminatedStringError struct {
Pos Pos
}
UnterminatedStringError is the error returned by the tokenizer when a string literal is never closed.
func (UnterminatedStringError) Error ¶
func (e UnterminatedStringError) Error() string
Error implements the [error] interface.