parser

package
v1.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package parser implements R7RS Scheme syntax parsing.

The parser converts a token stream into syntax values with source location:

Features

  • All Scheme datums: literals, symbols, lists, vectors, bytevectors
  • Full numeric tower: integers, floats, rationals, complex, big numbers
  • Quote forms: ', `, ,, ,@ and syntax variants #', #`, #,, #,@
  • Datum labels: #n= and #n# for shared/circular structures (R7RS 2.4)
  • Case folding: #!fold-case and #!no-fold-case directives
  • Symbol creation at parse time (with fold-case applied when active)

Usage

p := parser.NewParserWithFile(env, true, reader, "example.scm")
for {
    stx, err := p.ReadSyntax(ctx)
    if errors.Is(err, io.EOF) {
        break
    }
    // process stx
}

Error Handling

Parse errors are wrapped in ParserError with source location from the offending token. State that persists across expressions is deliberate and limited to fold-case mode (R7RS §2.1) and the trailing-EOF lookahead; datum labels are scoped to a single datum and cleared on every read (R7RS §2.4).

Index

Constants

View Source
const (
	ConstQuote            = "quote"
	ConstQuasiquote       = "quasiquote"
	ConstUnquote          = "unquote"
	ConstUnquoteSplicing  = "unquote-splicing"
	ConstSyntax           = "syntax"
	ConstQuasisyntax      = "quasisyntax"
	ConstUnsyntax         = "unsyntax"
	ConstUnsyntaxSplicing = "unsyntax-splicing"
)

Quote form identifiers.

View Source
const DefaultMaxParseDepth int = 10000

DefaultMaxParseDepth bounds structural nesting depth during parsing. Without a bound, adversarial input such as deeply nested parentheses triggers a fatal, unrecoverable Go stack overflow that kills the host process. 0 means unlimited. Mirrors the VM's DefaultMaxCallDepth.

Variables

View Source
var (
	// ErrUnknownTokenType is returned when the parser encounters an unrecognized token.
	ErrUnknownTokenType = werr.NewStaticError("unknown token type")
	ErrAlreadyClosed    = werr.NewStaticError("parser already closed")
)

Functions

func MakeExactFromLiteral added in v1.20.0

func MakeExactFromLiteral(text string, parsed values.Number) (values.Number, error)

MakeExactFromLiteral implements the #e prefix (R7RS §7.1.1), which applies to the number **as written** rather than to the value it would otherwise denote.

That distinction is the whole point of this function. A decimal literal is notation for a decimal value, so #e must convert its digits:

#e1e400  =>  10^400 exactly
#e0.1    =>  1/10

Reading the literal first and converting the result instead gives the nearest binary float's exact value, which is a different number:

(exact 1e400) =>  the 256-bit BigFloat neighbour of 10^400
(exact 0.1)   =>  3602879701896397/36028797018963968

Both answers are correct for their own question; only the first is #e. The procedure `exact` keeps the second, since by then the decimal is already gone.

text is the literal's source text with '#' digit placeholders already substituted; parsed is what reading it produced. Only Float and BigFloat (i.e. decimal or scientific notation) take the digits path; every other shape — already exact, radix-prefixed, complex — defers to MakeExactNumber, as does any text big.Rat rejects (inf, nan), which MakeExactNumber then refuses.

func MakeExactNumber added in v1.20.0

func MakeExactNumber(n values.Number) (values.Number, error)

MakeExactNumber returns the exact representation of n, implementing the #e prefix (R7RS §7.1.1) and the exact procedure (R7RS §6.2.6).

It is the single source of truth for the conversion, shared by the reader (parser_number.go makeExact) and string->number (extensions/math). The two must not diverge: a type handled by one and not the other silently drops the caller's #e prefix, which is exactly how *BigFloat came to be honored by the reader and ignored by string->number.

On a magnitude with no exact representation (inf, NaN, an inexact BigComplex) it returns a wrapped ErrExactnessConversion. Callers choose the policy: the reader wraps it with a source location, string->number discards it and yields #f per R7RS §6.2.7.

func MakeInexactNumber added in v1.20.0

func MakeInexactNumber(n values.Number) values.Number

MakeInexactNumber returns the inexact representation of n, implementing the #i prefix (R7RS §7.1.1), the '#' digit placeholder (§7.1.1), and the inexact procedure (§6.2.6). Companion to MakeExactNumber and likewise the single source of truth, shared by the reader and string->number.

It cannot fail: every exact number has an inexact image, and the already- inexact types (Float, BigFloat, Complex) are the identity. §6.2.6 sanctions silent precision loss here, so the accuracy/exact bools returned by big.Float and big.Rat are deliberately discarded.

func ParseComplexStringNumber

func ParseComplexStringNumber(s string) (values.Number, error)

ParseComplexStringNumber parses a rectangular complex number string ending in 'i'. Handles "3+4i", "1.5-2.5i", "1+inf.0i", "0+3/4i", etc.

On reject it returns a non-nil error carrying a werr sentinel (ErrInvalidFormat for a shape that is not this grammar; the part parser's own sentinel for a malformed real/imaginary part). See ParseImaginaryStringNumber for how the two callers treat the error differently.

func ParseImaginaryStringNumber

func ParseImaginaryStringNumber(s string) (values.Number, error)

ParseImaginaryStringNumber parses a pure imaginary string (ending in 'i') and returns the resulting complex number. Handles "+3i", "-2.5i", "+i", "-i", "+inf.0i", "-nan.0i", etc.

On reject it returns a non-nil error carrying a werr sentinel (ErrInvalidFormat for a shape that is not this grammar; the coefficient parser's own sentinel — ErrInvalidFormat / ErrDivisionByZero — for a malformed coefficient). Callers choose how to treat it: string->number discards it and tries the next grammar (R7RS §6.2.7 returns #f), while the reader wraps it with a source location so the cause stays reachable via errors.Is / errors.Unwrap.

func ParseRealFloatString

func ParseRealFloatString(s string) (values.Number, error)

ParseRealFloatString parses a real decimal or scientific-notation string as an inexact number. In-range magnitudes yield a float64-backed Float; magnitudes beyond float64 range (strconv reports ErrRange as ±Inf) promote to a BigFloat rather than failing. This mirrors the int64 -> BigInteger promotion in parseIntegerWithBase and, crucially, keeps the reader and string->number symmetric with the writer, which renders an out-of-range bigfloat in scientific notation (e.g. (write 1e1000) -> "1l+1000"). Underflow to zero (float64 rounds a tiny magnitude to 0 without ErrRange) is left as the float64 0.0 it already produces, matching float64-based Schemes. The exponent marker is normalized to 'e' first. Returns a wrapped ErrInvalidFormat sentinel on a malformed string.

func ParseRealFloatStringWithBase added in v1.20.0

func ParseRealFloatStringWithBase(text string, base int) (values.Number, error)

ParseRealFloatStringWithBase parses a real fraction written in an explicit radix, e.g. "1.8" in base 16 (1.5) or "101.101" in base 2 (5.625).

This is an extension, not conformance: R7RS §7.1.1 defines ⟨decimal R⟩ only for R = 10, so a radix-prefixed fraction is outside the standard grammar. Racket, Chez, and MIT all accept it, and this matches them.

Base 10 delegates to ParseRealFloatString so the decimal path — which every bare literal and every string->number call takes — stays exactly one implementation. Only the non-decimal bases are new code.

The 'p'/'P' rejection is not defensive clutter. big.ParseFloat accepts a *binary* exponent in a non-decimal base, so "1.8p3" in base 16 is 12, a grammar Scheme does not have. The reader cannot currently supply such a string (the tokenizer stops the numeral at 'p'; see requireDelimiterAfterRadixNumeral), but this function is exported next to parsers string->number already calls, so the guard belongs on the function rather than on its present caller. Note the asymmetry: 'e' is a hex *digit*, so big.ParseFloat("1e2", 16) is 482 — which is precisely what TestExponentMarkersStayDecimalOnly requires, and comes for free.

func ParseSpecialFloat

func ParseSpecialFloat(s string) (*values.Float, bool)

ParseSpecialFloat checks if s is +inf.0, -inf.0, +nan.0, or -nan.0 and returns the corresponding Float value. Returns (nil, false) if s is not a special-value string.

Types

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser represents a R7RS compliant Scheme syntax parser.

func NewParser

func NewParser(env *environment.EnvironmentFrame, skipComments bool, rdr io.RuneReader) *Parser

NewParser creates a new parser for the given reader and environment.

func NewParserWithFile

func NewParserWithFile(env *environment.EnvironmentFrame, skipComments bool, rdr io.RuneReader, file string) *Parser

NewParserWithFile creates a new parser with a specified source filename.

func (*Parser) Close

func (p *Parser) Close() error

Close closes the parser and releases resources. Every later ReadSyntax returns an error wrapping ErrAlreadyClosed, including on a parser that was never read from: releasing the tokenizer alone did not close anything, since ReadSyntax rebuilds one from the underlying reader whenever there is none.

Close's own return is unchanged: ErrAlreadyClosed if the parser is already closed or was never read from (the tokenizer is created lazily on the first ReadSyntax), otherwise the tokenizer's Close error.

func (*Parser) ReadSyntax

func (p *Parser) ReadSyntax(ctx context.Context) (syntax.SyntaxValue, error)

ReadSyntax reads and returns the next syntax value from the input. It honors ctx: a cancelled or deadline-exceeded context returns its error before reading the next form, so a long multi-form parse can be interrupted.

func (*Parser) SetFoldCase

func (p *Parser) SetFoldCase(on bool)

SetFoldCase enables or disables R7RS §2.1 fold-case mode at construction time, before any forms are read. This is the programmatic equivalent of a leading #!fold-case directive; it lets a caller (e.g. include-ci) read an entire file case-insensitively without the file itself carrying the directive. An in-file #!fold-case / #!no-fold-case directive still toggles the mode mid-stream.

func (*Parser) SetMaxDepth

func (p *Parser) SetMaxDepth(n int)

SetMaxDepth sets the maximum structural nesting depth allowed during parsing. A value of 0 (or negative, clamped to 0) disables the limit. Mirrors MachineContext.SetMaxCallDepth.

func (*Parser) Text

func (p *Parser) Text() string

Text returns the current text being parsed.

type ParserError

type ParserError struct {
	// contains filtered or unexported fields
}

ParserError represents an error that occurred during parsing.

The offending token (tok) carries the source position; file is the source name (empty for unnamed input such as a REPL stream). Location formats the two so the error's provenance survives to the final error chain — see REVIEW.md "Error Chain Losslessness".

func NewParserError

func NewParserError(tok tokenizer.Token, mess string) *ParserError

NewParserError creates a new parser error for the given token.

func NewParserErrorWithWrap

func NewParserErrorWithWrap(err error, tok tokenizer.Token, mess string) *ParserError

NewParserErrorWithWrap creates a new parser error wrapping another error.

func NewParserErrorWithWrapf

func NewParserErrorWithWrapf(err error, tok tokenizer.Token, mess string, vs ...any) *ParserError

NewParserErrorWithWrapf creates a new parser error wrapping another error.

func NewParserErrorf

func NewParserErrorf(tok tokenizer.Token, mess string, vs ...any) *ParserError

NewParserErrorf creates a new parser error for the given token.

func (*ParserError) EqualTo

func (p *ParserError) EqualTo(v values.Value) bool

func (*ParserError) Error

func (p *ParserError) Error() string

Error renders the message and, when the failure came from a lower layer, the cause's own text after it.

Rendering the cause is not cosmetic. locateReaderErr's fallback message is a fixed generic phrase; the entire diagnostic — which lexical rule was violated, at which index, on which character, in which tokenizer state — lives on the cause. Returning only mess left that reachable through errors.Unwrap but absent from every rendering, which is exactly the text-chain/unwrap-chain divergence REVIEW.md forbids.

A cause the message already contains is not repeated: a wrap that only adds a sentinel identity must not render as "x: x".

func (*ParserError) Is

func (p *ParserError) Is(err error) bool

Is implements errors.Is for ParserError. Two parser errors match when they carry the same message (the token is positional and is not part of error identity — see EqualTo for value-level equality that includes it), so errors.Is can distinguish parser failures; a wrapped cause is still reached through Unwrap.

func (*ParserError) IsVoid

func (p *ParserError) IsVoid() bool

func (*ParserError) Location

func (p *ParserError) Location() string

Location returns the offending token's position as "file:line:col", or "line:col" when the input is unnamed (e.g. a REPL stream, where the parser has no file name). It returns "" when the error has no located token.

Unlike SourceContext.Location, the unnamed-input form omits the leading colon ("1:0" rather than ":1:0"); line is 1-based and column 0-based, as in SourceContext.Location.

func (*ParserError) SchemeString

func (p *ParserError) SchemeString() string

SchemeString renders the error as a Scheme value. It goes through Error() so the cause's diagnostic reaches this surface too — a Scheme program that displays a read error must not see less than a Go caller does.

Only the location is added, and only because it is the one fact Error() does not carry: Error() reports index/line/column but not the file name. The offending token is deliberately NOT repeated here — Error() already names it.

func (*ParserError) Unwrap

func (p *ParserError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL