Packages
caffeine_lang
5.6.0
6.3.1
6.3.0
6.2.2
6.2.1
6.2.0
6.1.2
6.1.1
6.1.0
6.0.0
5.6.0
5.5.0
5.4.4
5.4.3
5.4.2
5.4.1
5.4.0
5.3.0
5.2.0
5.1.1
5.1.0
5.0.12
5.0.11
5.0.10
5.0.8
5.0.7
5.0.6
5.0.5
5.0.4
5.0.1
5.0.0
4.10.0
4.9.0
4.8.3
4.8.2
4.8.1
4.8.0
4.7.9
4.7.8
4.7.7
4.7.6
4.7.5
4.6.7
4.6.6
4.6.5
4.6.4
4.6.3
4.6.2
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.7
4.3.6
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.0.2
1.0.1
0.1.0
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.2
0.0.1
A compiler for generating reliability artifacts from service expectation definitions.
Current section
Files
Jump to
Current section
Files
src/caffeine_lang/frontend/parser.gleam
import caffeine_lang/frontend/ast.{
type Comment, type ExpectItem, type ExpectsBlock, type ExpectsFile,
type Extendable, type ExtendableKind, type Field, type Literal,
type MeasurementItem, type MeasurementsFile, type Parsed, type Struct,
type TypeAlias,
}
import caffeine_lang/frontend/parser_error.{type ParserError}
import caffeine_lang/frontend/token.{type PositionedToken, type Token}
import caffeine_lang/frontend/tokenizer
import caffeine_lang/types.{
type ParsedType, type PrimitiveTypes, type RefinementTypes, Boolean, Defaulted,
Dict, Float, InclusiveRange, Integer, List, NumericType, OneOf, Optional,
ParsedCollection, ParsedModifier, ParsedPrimitive, ParsedRecord,
ParsedRefinement, ParsedTypeAliasRef, Percentage, SemanticType,
String as StringType, URL,
}
import gleam/dict
import gleam/float
import gleam/int
import gleam/list
import gleam/option
import gleam/result
import gleam/set
import gleam/string
/// Parser state tracking position in token stream.
type ParserState {
ParserState(
tokens: List(PositionedToken),
line: Int,
column: Int,
prev_line: Int,
prev_column: Int,
)
}
/// Parses a measurements file from source text.
/// Returns all recoverable parse errors rather than stopping at the first one.
pub fn parse_measurements_file(
source: String,
) -> Result(MeasurementsFile(Parsed), List(ParserError)) {
use tokens <- result.try(
tokenizer.tokenize(source)
|> result.map_error(fn(e) { [parser_error.TokenizerError(e)] }),
)
let filtered = filter_whitespace(tokens)
let state = init_state(filtered)
let #(pending, state) = consume_comments(state)
use #(type_aliases, pending, state) <- result.try(
parse_type_aliases(state, pending)
|> result.map_error(fn(e) { [e] }),
)
use #(extendables, pending, state) <- result.try(
parse_extendables(state, pending)
|> result.map_error(fn(e) { [e] }),
)
let #(items, errors, pending, _state) =
parse_measurement_items_recovering(state, pending)
case errors {
[] ->
Ok(ast.MeasurementsFile(
type_aliases:,
extendables:,
items:,
trailing_comments: pending,
))
errs -> Error(errs)
}
}
/// Parses an expects file from source text.
/// Returns all recoverable parse errors rather than stopping at the first one.
pub fn parse_expects_file(
source: String,
) -> Result(ExpectsFile(Parsed), List(ParserError)) {
use tokens <- result.try(
tokenizer.tokenize(source)
|> result.map_error(fn(e) { [parser_error.TokenizerError(e)] }),
)
let filtered = filter_whitespace(tokens)
let state = init_state(filtered)
let #(pending, state) = consume_comments(state)
use #(extendables, pending, state) <- result.try(
parse_extendables(state, pending)
|> result.map_error(fn(e) { [e] }),
)
let #(blocks, errors, pending, _state) =
parse_expects_blocks_recovering(state, pending)
case errors {
[] -> Ok(ast.ExpectsFile(extendables:, blocks:, trailing_comments: pending))
errs -> Error(errs)
}
}
/// Filter out whitespace tokens (keep comments in stream).
fn filter_whitespace(tokens: List(PositionedToken)) -> List(PositionedToken) {
list.filter(tokens, fn(ptok) {
case ptok {
token.PositionedToken(token.WhitespaceNewline, _, _)
| token.PositionedToken(token.WhitespaceIndent(_), _, _) -> False
_ -> True
}
})
}
/// Consume consecutive comment tokens from the stream, returning them as Comment list.
fn consume_comments(state: ParserState) -> #(List(Comment), ParserState) {
consume_comments_loop(state, [])
}
fn consume_comments_loop(
state: ParserState,
acc: List(Comment),
) -> #(List(Comment), ParserState) {
case peek(state) {
token.CommentLine(text) ->
consume_comments_loop(advance(state), [ast.LineComment(text), ..acc])
token.CommentSection(text) ->
consume_comments_loop(advance(state), [ast.SectionComment(text), ..acc])
token.CommentDoc(text) ->
consume_comments_loop(advance(state), [ast.DocComment(text), ..acc])
_ -> #(list.reverse(acc), state)
}
}
/// Initialize parser state from a list of positioned tokens.
fn init_state(tokens: List(PositionedToken)) -> ParserState {
case tokens {
[token.PositionedToken(_, line, column), ..] ->
ParserState(tokens:, line:, column:, prev_line: line, prev_column: column)
[] -> ParserState(tokens:, line: 1, column: 1, prev_line: 1, prev_column: 1)
}
}
/// Peek at the current token without consuming it.
fn peek(state: ParserState) -> Token {
case state.tokens {
[token.PositionedToken(tok, _, _), ..] -> tok
[] -> token.EOF
}
}
/// Consume current token and advance state.
fn advance(state: ParserState) -> ParserState {
case state.tokens {
[_, ..rest] ->
case rest {
[token.PositionedToken(_, line, column), ..] ->
ParserState(
tokens: rest,
line:,
column:,
prev_line: state.line,
prev_column: state.column,
)
[] ->
ParserState(
..state,
tokens: rest,
prev_line: state.line,
prev_column: state.column,
)
}
[] -> state
}
}
/// Expect a specific token, consuming it if matched.
fn expect(
state: ParserState,
expected: Token,
expected_name: String,
) -> Result(ParserState, ParserError) {
case peek(state) {
tok if tok == expected -> Ok(advance(state))
tok ->
Error(parser_error.UnexpectedToken(
expected_name,
token.to_string(tok),
state.prev_line,
state.prev_column,
))
}
}
// =============================================================================
// TYPE ALIASES
// =============================================================================
/// Parse zero or more type aliases at file start.
/// Type alias syntax: _name (Type): <refinement_type>
fn parse_type_aliases(
state: ParserState,
pending: List(Comment),
) -> Result(#(List(TypeAlias), List(Comment), ParserState), ParserError) {
parse_type_aliases_loop(state, [], pending)
}
fn parse_type_aliases_loop(
state: ParserState,
acc: List(TypeAlias),
pending: List(Comment),
) -> Result(#(List(TypeAlias), List(Comment), ParserState), ParserError) {
case peek(state) {
token.Identifier(name) -> {
// Check if this is a type alias by peeking ahead for (Type)
case is_type_alias(state) {
True -> {
use #(type_alias, state) <- result.try(parse_type_alias(
state,
name,
pending,
))
let #(next_pending, state) = consume_comments(state)
parse_type_aliases_loop(state, [type_alias, ..acc], next_pending)
}
False -> Ok(#(list.reverse(acc), pending, state))
}
}
_ -> Ok(#(list.reverse(acc), pending, state))
}
}
/// Check if current position is a type alias definition.
/// Looks for pattern: Identifier ( KeywordType )
fn is_type_alias(state: ParserState) -> Bool {
case state.tokens {
[
token.PositionedToken(token.Identifier(_), _, _),
token.PositionedToken(token.SymbolLeftParen, _, _),
token.PositionedToken(token.KeywordType, _, _),
..
] -> True
_ -> False
}
}
fn parse_type_alias(
state: ParserState,
name: String,
leading_comments: List(Comment),
) -> Result(#(TypeAlias, ParserState), ParserError) {
// Validate type alias name - must be more than just underscore
case name {
"_" ->
Error(parser_error.InvalidTypeAliasName(
name,
"type alias name must have at least one character after the underscore",
state.line,
state.column,
))
_ -> {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolLeftParen, "("))
// Expect "Type" identifier
case peek(state) {
token.KeywordType -> {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolRightParen, ")"))
use state <- result.try(expect(state, token.SymbolColon, ":"))
// Parse the refinement type - must be a refined primitive type
use #(type_, state) <- result.try(parse_type(state))
Ok(#(ast.TypeAlias(name:, type_:, leading_comments:), state))
}
tok ->
Error(parser_error.UnexpectedToken(
"Type",
token.to_string(tok),
state.line,
state.column,
))
}
}
}
}
// =============================================================================
// EXTENDABLES
// =============================================================================
/// Parse zero or more extendables at file start.
fn parse_extendables(
state: ParserState,
pending: List(Comment),
) -> Result(#(List(Extendable), List(Comment), ParserState), ParserError) {
parse_extendables_loop(state, [], pending)
}
fn parse_extendables_loop(
state: ParserState,
acc: List(Extendable),
pending: List(Comment),
) -> Result(#(List(Extendable), List(Comment), ParserState), ParserError) {
case peek(state) {
token.Identifier(name) -> {
use #(extendable, state) <- result.try(parse_extendable(
state,
name,
pending,
))
let #(next_pending, state) = consume_comments(state)
parse_extendables_loop(state, [extendable, ..acc], next_pending)
}
_ -> Ok(#(list.reverse(acc), pending, state))
}
}
fn parse_extendable(
state: ParserState,
name: String,
leading_comments: List(Comment),
) -> Result(#(Extendable, ParserState), ParserError) {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolLeftParen, "("))
use #(kind, state) <- result.try(parse_extendable_kind(state))
use state <- result.try(expect(state, token.SymbolRightParen, ")"))
use state <- result.try(expect(state, token.SymbolColon, ":"))
// Parse body based on kind: Requires has types, Provides has literals.
use #(body, state) <- result.try(case kind {
ast.ExtendableRequires -> parse_type_struct(state)
ast.ExtendableProvides -> parse_literal_struct(state)
})
Ok(#(ast.Extendable(name:, kind:, body:, leading_comments:), state))
}
fn parse_extendable_kind(
state: ParserState,
) -> Result(#(ExtendableKind, ParserState), ParserError) {
case peek(state) {
token.KeywordRequires -> Ok(#(ast.ExtendableRequires, advance(state)))
token.KeywordProvides -> Ok(#(ast.ExtendableProvides, advance(state)))
tok ->
Error(parser_error.UnexpectedToken(
"Type, Requires, or Provides",
token.to_string(tok),
state.line,
state.column,
))
}
}
// =============================================================================
// MEASUREMENT ITEMS
// =============================================================================
fn parse_measurement_item(
state: ParserState,
leading_comments: List(Comment),
) -> Result(#(MeasurementItem, ParserState), ParserError) {
use #(name, state) <- result.try(parse_string_literal(state))
use #(extends, state) <- result.try(parse_optional_extends(state))
use state <- result.try(expect(state, token.SymbolColon, ":"))
use state <- result.try(expect(state, token.KeywordRequires, "Requires"))
use #(requires, state) <- result.try(parse_type_struct(state))
use state <- result.try(expect(state, token.KeywordProvides, "Provides"))
use #(provides, state) <- result.try(parse_literal_struct(state))
Ok(#(
ast.MeasurementItem(
name:,
extends:,
requires:,
provides:,
leading_comments:,
),
state,
))
}
// =============================================================================
// EXPECTS BLOCKS
// =============================================================================
fn parse_expect_item(
state: ParserState,
leading_comments: List(Comment),
) -> Result(#(ExpectItem, ParserState), ParserError) {
use state <- result.try(expect(state, token.SymbolStar, "*"))
use #(name, state) <- result.try(parse_string_literal(state))
use #(extends, state) <- result.try(parse_optional_extends(state))
use state <- result.try(expect(state, token.SymbolColon, ":"))
case peek(state) {
token.KeywordRequires ->
Error(parser_error.UnexpectedToken(
"Provides",
"Requires",
state.line,
state.column,
))
_ -> {
use state <- result.try(expect(state, token.KeywordProvides, "Provides"))
use #(provides, state) <- result.try(parse_literal_struct(state))
Ok(#(ast.ExpectItem(name:, extends:, provides:, leading_comments:), state))
}
}
}
// =============================================================================
// ERROR RECOVERY
// =============================================================================
/// Advance the state until `peek(state)` satisfies `predicate`.
/// Used by recovery loops to resync to the next item/block boundary.
fn skip_until(state: ParserState, predicate: fn(Token) -> Bool) -> ParserState {
case predicate(peek(state)) {
True -> state
False -> skip_until(advance(state), predicate)
}
}
fn at_block_boundary(tok: Token) -> Bool {
case tok {
token.KeywordExpectations | token.KeywordUnmeasured | token.EOF -> True
_ -> False
}
}
fn at_item_boundary(tok: Token) -> Bool {
case tok {
token.SymbolStar
| token.KeywordExpectations
| token.KeywordUnmeasured
| token.EOF -> True
_ -> False
}
}
fn at_measurement_item_boundary(tok: Token) -> Bool {
case tok {
token.LiteralString(_) | token.EOF -> True
_ -> False
}
}
/// Parse expects blocks with error recovery between blocks and items.
/// Recognizes both `Expectations measured by "..."` and `Unmeasured Expectations` headers.
fn parse_expects_blocks_recovering(
state: ParserState,
pending: List(Comment),
) -> #(List(ExpectsBlock), List(ParserError), List(Comment), ParserState) {
expects_blocks_loop(state, [], [], pending)
}
/// Custom block-level recovery loop that matches either KeywordExpectations or KeywordUnmeasured.
/// `error_acc` is accumulated in reverse order (cons head) and reversed at exit,
/// matching the convention in `measurement_items_loop` and `items_recovering_loop`.
fn expects_blocks_loop(
state: ParserState,
block_acc: List(ExpectsBlock),
error_acc: List(ParserError),
pending: List(Comment),
) -> #(List(ExpectsBlock), List(ParserError), List(Comment), ParserState) {
case peek(state) {
token.EOF -> #(
list.reverse(block_acc),
list.reverse(error_acc),
pending,
state,
)
token.KeywordExpectations | token.KeywordUnmeasured -> {
let #(block_result, item_errors, state) =
parse_expects_block_recovering(state, pending)
let #(more_comments, state) = consume_comments(state)
case block_result {
Ok(#(block, trailing)) -> {
let next_pending = list.append(trailing, more_comments)
expects_blocks_loop(
state,
[block, ..block_acc],
prepend_in_order(error_acc, item_errors),
next_pending,
)
}
Error(err) -> {
let state = skip_until(advance(state), at_block_boundary)
let #(next_pending, state) = consume_comments(state)
expects_blocks_loop(
state,
block_acc,
prepend_in_order(error_acc, [err, ..item_errors]),
next_pending,
)
}
}
}
tok -> {
let err =
parser_error.UnexpectedToken(
"Expectations or Unmeasured Expectations",
token.to_string(tok),
state.line,
state.column,
)
#(
list.reverse(block_acc),
list.reverse([err, ..error_acc]),
pending,
state,
)
}
}
}
/// Prepends `items` onto a reverse-order accumulator such that, after the
/// accumulator is reversed, the items appear in their original forward order.
fn prepend_in_order(reverse_acc: List(a), items: List(a)) -> List(a) {
list.fold(items, reverse_acc, fn(acc, item) { [item, ..acc] })
}
/// Parse a single expects block, recovering from item-level errors.
fn parse_expects_block_recovering(
state: ParserState,
leading_comments: List(Comment),
) -> #(
Result(#(ExpectsBlock, List(Comment)), ParserError),
List(ParserError),
ParserState,
) {
block_recovering(
state,
leading_comments,
parse_expects_block_header,
parse_expect_items_recovering,
fn(measurement, items, comments) {
ast.ExpectsBlock(measurement:, items:, leading_comments: comments)
},
)
}
/// Parse just the header of an expects block.
/// Handles both `Expectations measured by "name"` and `Unmeasured Expectations`.
fn parse_expects_block_header(
state: ParserState,
) -> Result(#(option.Option(String), ParserState), ParserError) {
case peek(state) {
token.KeywordUnmeasured -> {
let state = advance(state)
use state <- result.try(expect(
state,
token.KeywordExpectations,
"Expectations",
))
Ok(#(option.None, state))
}
_ -> {
use state <- result.try(expect(
state,
token.KeywordExpectations,
"Expectations",
))
use state <- result.try(expect(state, token.KeywordMeasured, "measured"))
use state <- result.try(expect(state, token.KeywordBy, "by"))
use #(measurement, state) <- result.try(parse_string_literal(state))
Ok(#(option.Some(measurement), state))
}
}
}
/// Generic single-block recovery parameterized by header parser, item parser, and block constructor.
fn block_recovering(
state: ParserState,
leading_comments: List(Comment),
parse_header: fn(ParserState) -> Result(#(header, ParserState), ParserError),
parse_items: fn(ParserState) ->
#(List(item), List(ParserError), List(Comment), ParserState),
build_block: fn(header, List(item), List(Comment)) -> block,
) -> #(
Result(#(block, List(Comment)), ParserError),
List(ParserError),
ParserState,
) {
case parse_header(state) {
Error(err) -> #(Error(err), [], state)
Ok(#(header_data, state)) -> {
let #(items, item_errors, trailing, state) = parse_items(state)
#(
Ok(#(build_block(header_data, items, leading_comments), trailing)),
item_errors,
state,
)
}
}
}
/// Parse measurement items with recovery between items.
/// Measurement items start with a string literal name (no `*` prefix).
fn parse_measurement_items_recovering(
state: ParserState,
pending: List(Comment),
) -> #(List(MeasurementItem), List(ParserError), List(Comment), ParserState) {
measurement_items_loop(state, [], [], pending)
}
/// Recovery loop for measurement items, detecting boundaries by string literal tokens.
fn measurement_items_loop(
state: ParserState,
items: List(MeasurementItem),
errors: List(ParserError),
pending: List(Comment),
) -> #(List(MeasurementItem), List(ParserError), List(Comment), ParserState) {
case peek(state) {
token.LiteralString(_) -> {
case parse_measurement_item(state, pending) {
Ok(#(item, state)) -> {
let #(next_pending, state) = consume_comments(state)
measurement_items_loop(state, [item, ..items], errors, next_pending)
}
Error(err) -> {
let state = skip_until(advance(state), at_measurement_item_boundary)
let #(next_pending, state) = consume_comments(state)
measurement_items_loop(state, items, [err, ..errors], next_pending)
}
}
}
_ -> #(list.reverse(items), list.reverse(errors), pending, state)
}
}
/// Parse expect items with recovery between items.
fn parse_expect_items_recovering(
state: ParserState,
) -> #(List(ExpectItem), List(ParserError), List(Comment), ParserState) {
let #(pending, state) = consume_comments(state)
items_recovering_loop(state, [], [], pending, parse_expect_item)
}
/// Generic item-level recovery loop parameterized by item parser.
fn items_recovering_loop(
state: ParserState,
item_acc: List(item),
error_acc: List(ParserError),
pending: List(Comment),
parse_item: fn(ParserState, List(Comment)) ->
Result(#(item, ParserState), ParserError),
) -> #(List(item), List(ParserError), List(Comment), ParserState) {
case peek(state) {
token.SymbolStar -> {
case parse_item(state, pending) {
Ok(#(item, state)) -> {
let #(next_pending, state) = consume_comments(state)
items_recovering_loop(
state,
[item, ..item_acc],
error_acc,
next_pending,
parse_item,
)
}
Error(err) -> {
let state = skip_until(advance(state), at_item_boundary)
let #(next_pending, state) = consume_comments(state)
items_recovering_loop(
state,
item_acc,
[err, ..error_acc],
next_pending,
parse_item,
)
}
}
}
_ -> #(list.reverse(item_acc), list.reverse(error_acc), pending, state)
}
}
// =============================================================================
// SHARED PARSING
// =============================================================================
fn parse_string_literal(
state: ParserState,
) -> Result(#(String, ParserState), ParserError) {
case peek(state) {
token.LiteralString(s) -> Ok(#(s, advance(state)))
tok ->
Error(parser_error.UnexpectedToken(
"string",
token.to_string(tok),
state.line,
state.column,
))
}
}
fn parse_optional_extends(
state: ParserState,
) -> Result(#(List(String), ParserState), ParserError) {
case peek(state) {
token.KeywordExtends -> {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolLeftBracket, "["))
use #(extends, state) <- result.try(parse_extends_list(state))
use state <- result.try(expect(state, token.SymbolRightBracket, "]"))
Ok(#(extends, state))
}
_ -> Ok(#([], state))
}
}
fn parse_extends_list(
state: ParserState,
) -> Result(#(List(String), ParserState), ParserError) {
case peek(state) {
token.SymbolRightBracket -> Ok(#([], state))
_ -> {
use #(first, state) <- result.try(parse_identifier(state))
sep_by_comma(state, [first], parse_identifier)
}
}
}
fn parse_identifier(
state: ParserState,
) -> Result(#(String, ParserState), ParserError) {
case peek(state) {
token.Identifier(name) -> Ok(#(name, advance(state)))
tok ->
Error(parser_error.UnexpectedToken(
"identifier",
token.to_string(tok),
state.line,
state.column,
))
}
}
/// Consume `, item , item ...` until a non-comma token. Does not allow
/// trailing commas (the parse_one callback runs after every comma).
fn sep_by_comma(
state: ParserState,
acc: List(a),
parse_one: fn(ParserState) -> Result(#(a, ParserState), ParserError),
) -> Result(#(List(a), ParserState), ParserError) {
case peek(state) {
token.SymbolComma -> {
let state = advance(state)
use #(item, state) <- result.try(parse_one(state))
sep_by_comma(state, [item, ..acc], parse_one)
}
_ -> Ok(#(list.reverse(acc), state))
}
}
// =============================================================================
// STRUCT PARSING (shared by Requires and Provides)
// =============================================================================
/// Type alias for a field value parser function.
type FieldValueParser(a) =
fn(ParserState) -> Result(#(a, ParserState), ParserError)
/// Parses a struct with typed fields (for Requires blocks).
fn parse_type_struct(
state: ParserState,
) -> Result(#(Struct, ParserState), ParserError) {
parse_struct(state, fn(s) {
use #(type_, s) <- result.try(parse_type(s))
Ok(#(ast.TypeValue(type_), s))
})
}
/// Parses a struct with literal fields (for Provides blocks).
fn parse_literal_struct(
state: ParserState,
) -> Result(#(Struct, ParserState), ParserError) {
parse_struct(state, fn(s) {
use #(literal, s) <- result.try(parse_literal(s))
Ok(#(ast.LiteralValue(literal), s))
})
}
/// Generic struct parser parameterized by field value parser.
fn parse_struct(
state: ParserState,
parse_value: FieldValueParser(ast.Value),
) -> Result(#(Struct, ParserState), ParserError) {
use state <- result.try(expect(state, token.SymbolLeftBrace, "{"))
let #(pending, state) = consume_comments(state)
use #(fields, trailing_comments, state) <- result.try(parse_fields(
state,
pending,
parse_value,
))
use state <- result.try(expect(state, token.SymbolRightBrace, "}"))
Ok(#(ast.Struct(fields:, trailing_comments:), state))
}
fn parse_fields(
state: ParserState,
pending: List(Comment),
parse_value: FieldValueParser(ast.Value),
) -> Result(#(List(Field), List(Comment), ParserState), ParserError) {
case peek(state) {
token.SymbolRightBrace -> Ok(#([], pending, state))
token.Identifier(_) -> {
use #(field, state) <- result.try(parse_field(state, pending, parse_value))
let #(next_pending, state) = consume_comments(state)
parse_fields_loop(state, [field], next_pending, parse_value)
}
// Helpful error for JSON-style quoted field names
token.LiteralString(name) ->
Error(parser_error.QuotedFieldName(name, state.line, state.column))
tok ->
Error(parser_error.UnexpectedToken(
"field name or }",
token.to_string(tok),
state.line,
state.column,
))
}
}
fn parse_fields_loop(
state: ParserState,
acc: List(Field),
pending: List(Comment),
parse_value: FieldValueParser(ast.Value),
) -> Result(#(List(Field), List(Comment), ParserState), ParserError) {
case peek(state) {
token.SymbolComma -> {
let state = advance(state)
let #(next_pending, state) = consume_comments(state)
// Allow trailing comma
case peek(state) {
token.SymbolRightBrace -> Ok(#(list.reverse(acc), next_pending, state))
_ -> {
use #(field, state) <- result.try(parse_field(
state,
next_pending,
parse_value,
))
let #(next_pending, state) = consume_comments(state)
parse_fields_loop(state, [field, ..acc], next_pending, parse_value)
}
}
}
_ -> Ok(#(list.reverse(acc), pending, state))
}
}
fn parse_field(
state: ParserState,
leading_comments: List(Comment),
parse_value: FieldValueParser(ast.Value),
) -> Result(#(Field, ParserState), ParserError) {
case peek(state) {
token.Identifier(name) -> {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolColon, ":"))
use #(value, state) <- result.try(parse_value(state))
Ok(#(ast.Field(name:, value:, leading_comments:), state))
}
// Helpful error for JSON-style quoted field names
token.LiteralString(name) ->
Error(parser_error.QuotedFieldName(name, state.line, state.column))
tok ->
Error(parser_error.UnexpectedToken(
"field name",
token.to_string(tok),
state.line,
state.column,
))
}
}
// =============================================================================
// TYPES
// =============================================================================
/// Match a primitive type keyword and consume it. Returns Error(Nil) on miss.
fn try_parse_primitive_keyword(
state: ParserState,
) -> Result(#(PrimitiveTypes, ParserState), Nil) {
case peek(state) {
token.KeywordString -> Ok(#(StringType, advance(state)))
token.KeywordInteger -> Ok(#(NumericType(Integer), advance(state)))
token.KeywordFloat -> Ok(#(NumericType(Float), advance(state)))
token.KeywordBoolean -> Ok(#(Boolean, advance(state)))
token.KeywordURL -> Ok(#(SemanticType(URL), advance(state)))
token.KeywordPercentage -> Ok(#(NumericType(Percentage), advance(state)))
_ -> Error(Nil)
}
}
/// Parse a type-alias reference (`_name`) or surface UnknownType for a bare identifier.
fn parse_type_alias_ref_or_error(
state: ParserState,
name: String,
) -> Result(#(ParsedType, ParserState), ParserError) {
case string.starts_with(name, "_") {
True -> Ok(#(ParsedTypeAliasRef(name), advance(state)))
False -> Error(parser_error.UnknownType(name, state.line, state.column))
}
}
fn parse_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
case try_parse_primitive_keyword(state) {
Ok(#(primitive, state)) ->
case peek(state) {
token.SymbolLeftBrace -> {
use #(refinement, state) <- result.try(parse_refinement(
state,
primitive,
))
Ok(#(ParsedRefinement(refinement), state))
}
_ -> Ok(#(ParsedPrimitive(primitive), state))
}
Error(_) ->
case peek(state) {
token.KeywordList -> parse_list_type(state)
token.KeywordDict -> parse_dict_type(state)
token.KeywordOptional -> parse_optional_type(state)
token.KeywordDefaulted -> parse_defaulted_type(state)
token.SymbolLeftBrace -> parse_record_type(state)
token.Identifier(name) -> parse_type_alias_ref_or_error(state, name)
tok ->
Error(parser_error.UnknownType(
token.to_string(tok),
state.line,
state.column,
))
}
}
}
/// Parses a record type: `{ field: Type, ... }`.
/// Reuses parse_type_struct to parse the struct, then converts fields to a dict.
fn parse_record_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
use #(s, state) <- result.try(parse_type_struct(state))
let fields =
s.fields
|> list.map(fn(field) {
let assert ast.TypeValue(t) = field.value
#(field.name, t)
})
|> dict.from_list
Ok(#(ParsedRecord(fields), state))
}
/// Parses types valid inside collections: primitives, nested collections, or type alias refs.
/// Does not allow modifiers (Optional/Defaulted) or refinements directly.
fn parse_collection_inner_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
case try_parse_primitive_keyword(state) {
Ok(#(primitive, state)) -> Ok(#(ParsedPrimitive(primitive), state))
Error(_) ->
case peek(state) {
token.KeywordList -> parse_list_type(state)
token.KeywordDict -> parse_dict_type(state)
token.SymbolLeftBrace -> parse_record_type(state)
token.Identifier(name) -> parse_type_alias_ref_or_error(state, name)
tok ->
Error(parser_error.UnknownType(
token.to_string(tok),
state.line,
state.column,
))
}
}
}
fn parse_list_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolLeftParen, "("))
use #(element, state) <- result.try(parse_collection_inner_type(state))
use state <- result.try(expect(state, token.SymbolRightParen, ")"))
Ok(#(ParsedCollection(List(element)), state))
}
fn parse_dict_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolLeftParen, "("))
// Dict keys must be String primitive or type alias ref (for JSON compatibility)
use #(key, state) <- result.try(parse_dict_key_type(state))
use state <- result.try(expect(state, token.SymbolComma, ","))
// Dict values can be primitives, nested collections, or type alias refs
use #(value, state) <- result.try(parse_collection_inner_type(state))
use state <- result.try(expect(state, token.SymbolRightParen, ")"))
Ok(#(ParsedCollection(Dict(key, value)), state))
}
/// Parses types valid as Dict keys: String primitive or type alias ref.
/// Only String is allowed as a primitive key (JSON keys must be strings).
fn parse_dict_key_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
case peek(state) {
token.KeywordString -> Ok(#(ParsedPrimitive(StringType), advance(state)))
token.Identifier(name) -> parse_type_alias_ref_or_error(state, name)
tok ->
Error(parser_error.UnknownType(
token.to_string(tok),
state.line,
state.column,
))
}
}
fn parse_optional_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolLeftParen, "("))
use #(inner, state) <- result.try(parse_type(state))
use state <- result.try(expect(state, token.SymbolRightParen, ")"))
Ok(#(ParsedModifier(Optional(inner)), state))
}
fn parse_defaulted_type(
state: ParserState,
) -> Result(#(ParsedType, ParserState), ParserError) {
let state = advance(state)
use state <- result.try(expect(state, token.SymbolLeftParen, "("))
use #(inner, state) <- result.try(parse_type(state))
use state <- result.try(expect(state, token.SymbolComma, ","))
use #(default, state) <- result.try(parse_literal(state))
use state <- result.try(expect(state, token.SymbolRightParen, ")"))
let defaulted = ParsedModifier(Defaulted(inner, literal_to_string(default)))
// Check for optional refinement: Defaulted(String, "x") { x | x in { ... } }
case peek(state) {
token.SymbolLeftBrace -> {
use #(refinement, state) <- result.try(parse_refinement_with_inner(
state,
defaulted,
))
Ok(#(ParsedRefinement(refinement), state))
}
_ -> Ok(#(defaulted, state))
}
}
// =============================================================================
// REFINEMENTS
// =============================================================================
/// Parse `{ x | x in <body> }` for a primitive base type.
fn parse_refinement(
state: ParserState,
primitive: PrimitiveTypes,
) -> Result(#(RefinementTypes(ParsedType), ParserState), ParserError) {
parse_refinement_with_inner(state, ParsedPrimitive(primitive))
}
/// Parse `{ x | x in <body> }` wrapping an arbitrary inner type. Shared
/// implementation for primitive refinements and Defaulted-with-refinement.
fn parse_refinement_with_inner(
state: ParserState,
inner: ParsedType,
) -> Result(#(RefinementTypes(ParsedType), ParserState), ParserError) {
use state <- result.try(expect(state, token.SymbolLeftBrace, "{"))
use state <- result.try(expect_x(state))
use state <- result.try(expect(state, token.SymbolPipe, "|"))
use state <- result.try(expect_x(state))
use state <- result.try(expect(state, token.KeywordIn, "in"))
use #(refinement, state) <- result.try(parse_refinement_body(state, inner))
use state <- result.try(expect(state, token.SymbolRightBrace, "}"))
Ok(#(refinement, state))
}
fn expect_x(state: ParserState) -> Result(ParserState, ParserError) {
case peek(state) {
token.KeywordX -> Ok(advance(state))
tok ->
Error(parser_error.InvalidRefinement(
"expected 'x', got " <> token.to_string(tok),
state.line,
state.column,
))
}
}
fn parse_refinement_body(
state: ParserState,
inner: ParsedType,
) -> Result(#(RefinementTypes(ParsedType), ParserState), ParserError) {
case peek(state) {
// OneOf: { value1, value2, ... }
token.SymbolLeftBrace -> {
let state = advance(state)
use #(values, state) <- result.try(parse_literal_list_contents(state))
use state <- result.try(expect(state, token.SymbolRightBrace, "}"))
let string_values = list.map(values, literal_to_string)
Ok(#(OneOf(inner, set.from_list(string_values)), state))
}
// Range: ( min..max )
token.SymbolLeftParen -> {
let state = advance(state)
use #(min, state) <- result.try(parse_literal(state))
use state <- result.try(expect(state, token.SymbolDotDot, ".."))
use #(max, state) <- result.try(parse_literal(state))
use state <- result.try(expect(state, token.SymbolRightParen, ")"))
Ok(#(
InclusiveRange(inner, literal_to_string(min), literal_to_string(max)),
state,
))
}
tok ->
Error(parser_error.UnexpectedToken(
"{ or (",
token.to_string(tok),
state.line,
state.column,
))
}
}
// =============================================================================
// LITERALS
// =============================================================================
fn parse_literal(
state: ParserState,
) -> Result(#(Literal, ParserState), ParserError) {
case peek(state) {
token.LiteralString(s) -> Ok(#(ast.LiteralString(s), advance(state)))
token.LiteralInteger(n) -> Ok(#(ast.LiteralInteger(n), advance(state)))
token.LiteralFloat(f) -> Ok(#(ast.LiteralFloat(f), advance(state)))
token.LiteralPercentage(f) ->
Ok(#(ast.LiteralPercentage(f), advance(state)))
token.LiteralTrue -> Ok(#(ast.LiteralTrue, advance(state)))
token.LiteralFalse -> Ok(#(ast.LiteralFalse, advance(state)))
token.SymbolLeftBracket -> parse_literal_list(state)
token.SymbolLeftBrace -> parse_literal_struct_value(state)
tok ->
Error(parser_error.UnexpectedToken(
"literal value",
token.to_string(tok),
state.line,
state.column,
))
}
}
fn parse_literal_list(
state: ParserState,
) -> Result(#(Literal, ParserState), ParserError) {
use state <- result.try(expect(state, token.SymbolLeftBracket, "["))
use #(elements, state) <- result.try(parse_literal_list_contents(state))
use state <- result.try(expect(state, token.SymbolRightBracket, "]"))
Ok(#(ast.LiteralList(elements), state))
}
fn parse_literal_list_contents(
state: ParserState,
) -> Result(#(List(Literal), ParserState), ParserError) {
case peek(state) {
token.SymbolRightBracket | token.SymbolRightBrace -> Ok(#([], state))
_ -> {
use #(first, state) <- result.try(parse_literal(state))
sep_by_comma(state, [first], parse_literal)
}
}
}
fn parse_literal_struct_value(
state: ParserState,
) -> Result(#(Literal, ParserState), ParserError) {
let parse_value = fn(s) {
use #(literal, s) <- result.try(parse_literal(s))
Ok(#(ast.LiteralValue(literal), s))
}
use state <- result.try(expect(state, token.SymbolLeftBrace, "{"))
let #(pending, state) = consume_comments(state)
use #(fields, trailing_comments, state) <- result.try(parse_fields(
state,
pending,
parse_value,
))
use state <- result.try(expect(state, token.SymbolRightBrace, "}"))
Ok(#(ast.LiteralStruct(fields, trailing_comments), state))
}
/// Convert a Literal to its string representation for use in refinements/defaults.
/// List literals are serialized as "[a, b, c]" using the same element format as
/// types.gleam's value-to-string, so list defaults can be round-tripped at the
/// resolver. Round-trip via comma-split does not preserve commas embedded inside
/// individual string elements.
fn literal_to_string(literal: Literal) -> String {
case literal {
ast.LiteralString(s) -> s
ast.LiteralInteger(n) -> int.to_string(n)
ast.LiteralFloat(f) -> float.to_string(f)
ast.LiteralPercentage(f) -> float.to_string(f) <> "%"
ast.LiteralTrue -> "true"
ast.LiteralFalse -> "false"
ast.LiteralList(elements) ->
"[" <> elements |> list.map(literal_to_string) |> string.join(", ") <> "]"
ast.LiteralStruct(_, _) -> "{}"
}
}