Syntax.Cursor
The mechanical half of recursive descent: a token cursor with peek, advance, and expect, and a Pratt driver over a binding-power table.
Deliberately not a combinator library. There is no alternation operator, no backtracking engine, no grammar vocabulary; a caller writes the grammar by hand, in ordinary functions, and hands this module the two things a hand-written parser gets subtly wrong. The first is span arithmetic: joining two spans, covering a consumed run, and pointing a caret at a position where nothing was read. The second is expectation merging: unmet expectations join a set held at the furthest token the cursor ever reached, so a refusal names every alternative tried at the position the parse died at rather than only the last one attempted. Rewinding is where that record is classically lost, so rewinding is a named operation (cursor_restore) that returns the position and keeps the record.
Expectations are token wire names, the vocabulary of Syntax.Token, and a refusal renders as a Syntax.Diagnostic under the compiler’s own parse codes. A parser built on this cursor therefore reports through the same document every other syntax fault does, with no error type of its own.
Types
Cursor
type Cursor = Cursor {
toks: Array(Token),
pos: Int,
stop: Int,
far: Int,
wanted: List(String)
}
A token cursor: the tokens being read, the read position, the byte offset the end-of-input caret points at, and the failure record (the furthest position at which an expectation was recorded, and the names recorded there). The failure record is part of the cursor rather than of a failure value because it must outlive every failure a parse recovers from.
Reply
type Reply(a) = Took(a, Cursor) | Stuck(Cursor)
The reply of a cursor step: the value and the advanced cursor, or the cursor alone. A refusal still carries a cursor, because the failure record it accumulated is the part worth reporting.
Assoc
type Assoc = ALeft | ARight | ANone deriving (Eq)
How an infix operator groups a chain of its own level. A non-associative operator declines a second application at its level, leaving the second operator unconsumed for the caller to refuse.
Infix
type Infix = Infix { op: String, level: Int, assoc: Assoc }
One infix operator: the token spelling, its level (higher binds tighter), and how it associates.
Prefix
type Prefix = Prefix { op: String, level: Int }
One prefix operator: the token spelling and the level its operand is parsed at. An operator that binds tighter than that level is pulled into the operand; one that binds looser takes the prefix application as its left operand.
Pratt
type Pratt(a) = Pratt {
infixes: List(Infix),
prefixes: List(Prefix),
leaf: (Cursor) -> Reply(a),
bin: (String, a, a) -> a,
pre: (String, a) -> a
}
Everything the driver needs that is not the cursor: the two operator tables, the leaf parser, and the two node builders. The leaf parser owns primaries, and so owns parentheses, and so owns the recursion back into the driver: the driver knows about operators and nothing else.
Functions and Values
span_join
span_join : (Syntax.Source.Span, Syntax.Source.Span) -> Syntax.Source.Span
The smallest span covering both: the earlier start and the later end. The one operation a parser needs to give a node built from parts the extent of those parts.
let s = span_join(span_at(2), span_at(11))
(s.lo, s.hi)
(2, 11)
span_at
span_at : (Int) -> Syntax.Source.Span
The empty span at a byte offset: the caret a diagnostic points with when it has a position but nothing was consumed there.
let s = span_at(12)
(s.lo, s.hi)
(12, 12)
span_cover
span_cover : (List(Syntax.Source.Span)) -> Option(Syntax.Source.Span)
The smallest span covering every span in the list, or None for the empty list. Joining has no identity over spans (the empty span at offset zero is a position, not a neutral element), so the empty case is absent rather than invented.
cursor_of
cursor_of : (List(Syntax.Token.Token), Int) -> Syntax.Cursor.Cursor
A cursor over a token list. stop is the byte offset the end-of-input caret points at, normally the length of the source the tokens were lexed from, so a refusal past the last token still carries a position.
cursor_count
cursor_count : (Syntax.Cursor.Cursor) -> Int
The number of tokens the cursor reads over.
cursor_at_end
cursor_at_end : (Syntax.Cursor.Cursor) -> Bool
Whether every token has been consumed.
cursor_peek_at
cursor_peek_at : (Syntax.Cursor.Cursor, Int) -> Option(Syntax.Token.Token)
The token k positions ahead of the read position, or None past the end.
cursor_peek
cursor_peek : (Syntax.Cursor.Cursor) -> Option(Syntax.Token.Token)
The token at the read position, or None at the end. Peeking never consumes and never records an expectation: a parser that looks before it leaps says what it wanted with cursor_note.
cursor_kind
cursor_kind : (Syntax.Cursor.Cursor) -> Option(Syntax.Token.TokenKind)
The kind of the token at the read position, or None at the end.
cursor_span
cursor_span : (Syntax.Cursor.Cursor) -> Syntax.Source.Span
The span the cursor points at: the next token’s span, or the end-of-input caret when the stream is exhausted.
cursor_advance
cursor_advance : (Syntax.Cursor.Cursor) -> Syntax.Cursor.Cursor
The cursor advanced past one token. At the end this is the identity, so a parser that advances on a token it did not check cannot run off the stream.
cursor_since
cursor_since : (Syntax.Cursor.Cursor, Syntax.Cursor.Cursor) -> Syntax.Source.Span
The span covering the tokens consumed between saved and c, or the caret at the current position when nothing was consumed. A parser gives a node its extent by saving the cursor before it and asking for the run after it, rather than by joining the spans of its parts by hand and forgetting the delimiters.
end_of_input
end_of_input : () -> String
The name the end of the token stream is expected under. Not a token, so not a token kind, but an expectation like any other.
cursor_note_name
cursor_note_name : (Syntax.Cursor.Cursor, String) -> Syntax.Cursor.Cursor
Record name as admissible at the read position, consuming nothing.
The record only ever moves forward. A name recorded past the furthest position reached replaces the set and moves the mark; a name at the furthest position joins the set; a name behind it is dropped. That ordering is the whole point: a parse that tries an alternative, gets three tokens deep and fails, then rewinds, must still report what was wanted three tokens in, not what was wanted at the place it rewound to.
cursor_note
cursor_note : (Syntax.Cursor.Cursor, Syntax.Token.TokenKind) -> Syntax.Cursor.Cursor
Record a token kind as admissible at the read position. The name recorded is the kind’s wire name, so an expectation set, a token artifact, and a diagnostic’s expected field all speak one vocabulary.
cursor_expect
cursor_expect : (Syntax.Cursor.Cursor, Syntax.Token.TokenKind) -> Syntax.Cursor.Reply(Syntax.Token.Token)
Consume the next token when its kind is k. On a mismatch nothing is consumed and k joins the expectation set at this position, so a parser that tries several kinds in turn accumulates all of them: expecting one of a set is spelled as expecting each of its members at one position, never as a separate combinator.
cursor_expect_fixed
cursor_expect_fixed : (Syntax.Cursor.Cursor, String) -> Syntax.Cursor.Reply(Syntax.Token.Token)
Consume the next token when it is the fixed token spelled s.
cursor_expect_end
cursor_expect_end : (Syntax.Cursor.Cursor) -> Syntax.Cursor.Reply(Unit)
Require that every token has been consumed, recording the end of the stream as the expectation when one is left over.
cursor_restore
cursor_restore : (Syntax.Cursor.Cursor, Syntax.Cursor.Cursor) -> Syntax.Cursor.Cursor
Rewind to a saved cursor while keeping the failure record reached since.
A parser that tries an alternative and gives it up restores with this: the position returns, the expectations do not. Rebuilding the saved cursor by hand instead is the classic way to report the expectations of the position the parse retreated to rather than of the position it died at.
let c0 = cursor_of(Nil, 0)
let c1 = cursor_note_name(c0, "ident")
str_join(", ", cursor_expected(cursor_restore(c0, c1)))
ident
cursor_expected
cursor_expected : (Syntax.Cursor.Cursor) -> List(String)
The names recorded at the furthest position reached, in the order they were first recorded.
let c = cursor_note_name(cursor_of(Nil, 3), "ident")
str_join(", ", cursor_expected(cursor_note_name(c, "int")))
ident, int
cursor_canonical
cursor_canonical : (Syntax.Cursor.Cursor) -> List(String)
The canonical expectation set of a refusal: the recorded names deduplicated and byte-lexicographically sorted, so the set a diagnostic carries does not depend on the order alternatives were tried, and a harmless reordering of a parser’s branches moves no artifact bytes.
let c = cursor_note_name(cursor_of(Nil, 3), "int")
str_join(", ", cursor_canonical(cursor_note_name(c, "ident")))
ident, int
cursor_far_span
cursor_far_span : (Syntax.Cursor.Cursor) -> Syntax.Source.Span
The span the failure record points at: the token at the furthest position reached, or the end-of-input caret.
cursor_message
cursor_message : (Syntax.Cursor.Cursor) -> String
The rendered message of an unmet expectation: the merged expectation set and what stood there instead.
parse_syntax_code
parse_syntax_code : () -> String
The compiler’s general parse-fault code, the one home for the string on this side of the seam.
parse_eof_code
parse_eof_code : () -> String
The compiler’s exhausted-stream code: the token stream itself ended while the parser wanted more.
cursor_code
cursor_code : (Syntax.Cursor.Cursor) -> String
The stable code an unmet expectation carries: the parser’s end-of-input code when the furthest position reached is past the last token, and its general syntax code otherwise. Both match the compiler’s own parse codes.
cursor_diagnostic
cursor_diagnostic : (Syntax.Cursor.Cursor) -> Syntax.Diagnostic.Diagnostic
The refusal an unmet expectation produces: a parse-phase diagnostic at the furthest position reached, carrying the canonical expectation set. The message keeps the order alternatives were tried, for a reader; the set is sorted, for the artifact. A cursor failure is a diagnostic and never a bespoke error value, so a hand-written parser’s refusals join the same document the compiler’s own do.
infix_left_bp
infix_left_bp : (Syntax.Cursor.Infix) -> Int
The power binding an infix operator’s left operand.
Levels are doubled so each has a half step above it. A left-associative operator’s right power sits in that half step, which stops a second operator of the same level from being pulled into the right operand; a right associative operator’s left power sits there instead, which lets it be. A non-associative operator is left-shaped here and refuses the repeat outright.
infix_right_bp
infix_right_bp : (Syntax.Cursor.Infix) -> Int
The power an infix operator’s right operand is parsed at.
prefix_bp
prefix_bp : (Syntax.Cursor.Prefix) -> Int
The power a prefix operator’s operand is parsed at.
infix_lookup
infix_lookup : (List(Syntax.Cursor.Infix), String) -> Option(Syntax.Cursor.Infix)
The entry for an operator spelling in an infix table, or None when the spelling is not an infix operator.
prefix_lookup
prefix_lookup : (List(Syntax.Cursor.Prefix), String) -> Option(Syntax.Cursor.Prefix)
The entry for an operator spelling in a prefix table, or None when the spelling is not a prefix operator.
pratt_expr
pratt_expr : forall a. (Syntax.Cursor.Pratt(a), Syntax.Cursor.Cursor, Int) -> Syntax.Cursor.Reply(a)
Parse one expression whose operators all bind at least as tightly as min_bp: one operand, then every operator that outbinds the caller.
pratt_parse
pratt_parse : forall a. (Syntax.Cursor.Pratt(a), Syntax.Cursor.Cursor) -> Syntax.Cursor.Reply(a)
Parse one expression at the loosest binding power. Tokens after it are left for the caller, which is what makes this usable inside a larger grammar.
pratt_all
pratt_all : forall a. (Syntax.Cursor.Pratt(a), Syntax.Cursor.Cursor) -> Syntax.Cursor.Reply(a)
Parse one expression and require the whole token stream to be consumed. A trailing operator the driver declined (a repeated non-associative one) is refused here, at the position it stands.
expr_infix_table
expr_infix_table : () -> List(Syntax.Cursor.Infix)
The infix table of Prism’s own expression grammar, loosest first. Levels are relative: only their order and each operator’s associativity carry meaning, and both are pinned against the compiler’s parser by a differential test rather than by this comment.
expr_prefix_table
expr_prefix_table : () -> List(Syntax.Cursor.Prefix)
The prefix table of Prism’s own expression grammar: unary minus, sitting between multiplication and exponentiation. -x * y is (-x) * y and, by the mathematical convention, -x ^ y is -(x ^ y).