import caffeine_lang/frontend/ast.{ type BlueprintItem, type BlueprintsBlock, type BlueprintsFile, type Comment, type ExpectItem, type ExpectsBlock, type ExpectsFile, type Extendable, type ExtendableKind, type Field, type Literal, type ParsedArtifactRef, 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, SemanticType, String as StringType, URL, } import gleam/dict import gleam/float import gleam/int import gleam/list 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 blueprints file from source text. pub fn parse_blueprints_file( source: String, ) -> Result(BlueprintsFile, ParserError) { use tokens <- result.try( tokenizer.tokenize(source) |> result.map_error(parser_error.TokenizerError), ) 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, )) use #(extendables, pending, state) <- result.try(parse_extendables( state, pending, )) use #(blocks, pending, _state) <- result.try(parse_blueprints_blocks( state, pending, )) Ok(ast.BlueprintsFile( type_aliases:, extendables:, blocks:, trailing_comments: pending, )) } /// Parses an expects file from source text. pub fn parse_expects_file(source: String) -> Result(ExpectsFile, ParserError) { use tokens <- result.try( tokenizer.tokenize(source) |> result.map_error(parser_error.TokenizerError), ) 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, )) use #(blocks, pending, _state) <- result.try(parse_expects_blocks( state, pending, )) Ok(ast.ExpectsFile(extendables:, blocks:, trailing_comments: pending)) } /// 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]) _ -> #(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): 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, )) } } // ============================================================================= // BLUEPRINTS BLOCKS // ============================================================================= /// Parse zero or more blueprints blocks. fn parse_blueprints_blocks( state: ParserState, pending: List(Comment), ) -> Result(#(List(BlueprintsBlock), List(Comment), ParserState), ParserError) { parse_blueprints_blocks_loop(state, [], pending) } fn parse_blueprints_blocks_loop( state: ParserState, acc: List(BlueprintsBlock), pending: List(Comment), ) -> Result(#(List(BlueprintsBlock), List(Comment), ParserState), ParserError) { case peek(state) { token.KeywordBlueprints -> { use #(block, trailing, state) <- result.try(parse_blueprints_block( state, pending, )) let #(more_comments, state) = consume_comments(state) let next_pending = list.append(trailing, more_comments) parse_blueprints_blocks_loop(state, [block, ..acc], next_pending) } token.EOF -> Ok(#(list.reverse(acc), pending, state)) tok -> Error(parser_error.UnexpectedToken( "Blueprints", token.to_string(tok), state.line, state.column, )) } } fn parse_blueprints_block( state: ParserState, leading_comments: List(Comment), ) -> Result(#(BlueprintsBlock, List(Comment), ParserState), ParserError) { use state <- result.try(expect(state, token.KeywordBlueprints, "Blueprints")) use state <- result.try(expect(state, token.KeywordFor, "for")) use #(artifacts, state) <- result.try(parse_artifacts(state)) use #(items, trailing, state) <- result.try(parse_blueprint_items(state)) Ok(#( ast.BlueprintsBlock(artifacts:, items:, leading_comments:), trailing, state, )) } fn parse_artifacts( state: ParserState, ) -> Result(#(List(ParsedArtifactRef), ParserState), ParserError) { case peek(state) { token.LiteralString(name) -> { use ref <- result.try(resolve_artifact_ref(name, state)) let state = advance(state) parse_artifacts_loop(state, [ref]) } tok -> Error(parser_error.UnexpectedToken( "artifact name", token.to_string(tok), state.line, state.column, )) } } fn parse_artifacts_loop( state: ParserState, acc: List(ParsedArtifactRef), ) -> Result(#(List(ParsedArtifactRef), ParserState), ParserError) { case peek(state) { token.SymbolPlus -> { let state = advance(state) case peek(state) { token.LiteralString(name) -> { use ref <- result.try(resolve_artifact_ref(name, state)) let state = advance(state) parse_artifacts_loop(state, [ref, ..acc]) } tok -> Error(parser_error.UnexpectedToken( "artifact name", token.to_string(tok), state.line, state.column, )) } } _ -> Ok(#(list.reverse(acc), state)) } } /// Resolves an artifact name string to a ParsedArtifactRef. fn resolve_artifact_ref( name: String, state: ParserState, ) -> Result(ParsedArtifactRef, ParserError) { case name { "SLO" -> Ok(ast.ParsedSLO) "DependencyRelations" -> Ok(ast.ParsedDependencyRelations) _ -> Error(parser_error.UnexpectedToken( "\"SLO\" or \"DependencyRelations\"", "\"" <> name <> "\"", state.line, state.column, )) } } fn parse_blueprint_items( state: ParserState, ) -> Result(#(List(BlueprintItem), List(Comment), ParserState), ParserError) { let #(pending, state) = consume_comments(state) parse_blueprint_items_loop(state, [], pending) } fn parse_blueprint_items_loop( state: ParserState, acc: List(BlueprintItem), pending: List(Comment), ) -> Result(#(List(BlueprintItem), List(Comment), ParserState), ParserError) { case peek(state) { token.SymbolStar -> { use #(item, state) <- result.try(parse_blueprint_item(state, pending)) let #(next_pending, state) = consume_comments(state) parse_blueprint_items_loop(state, [item, ..acc], next_pending) } _ -> Ok(#(list.reverse(acc), pending, state)) } } fn parse_blueprint_item( state: ParserState, leading_comments: List(Comment), ) -> Result(#(BlueprintItem, 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, ":")) 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.BlueprintItem(name:, extends:, requires:, provides:, leading_comments:), state, )) } // ============================================================================= // EXPECTS BLOCKS // ============================================================================= /// Parse zero or more expects blocks. fn parse_expects_blocks( state: ParserState, pending: List(Comment), ) -> Result(#(List(ExpectsBlock), List(Comment), ParserState), ParserError) { parse_expects_blocks_loop(state, [], pending) } fn parse_expects_blocks_loop( state: ParserState, acc: List(ExpectsBlock), pending: List(Comment), ) -> Result(#(List(ExpectsBlock), List(Comment), ParserState), ParserError) { case peek(state) { token.KeywordExpectations -> { use #(block, trailing, state) <- result.try(parse_expects_block( state, pending, )) let #(more_comments, state) = consume_comments(state) let next_pending = list.append(trailing, more_comments) parse_expects_blocks_loop(state, [block, ..acc], next_pending) } token.EOF -> Ok(#(list.reverse(acc), pending, state)) tok -> Error(parser_error.UnexpectedToken( "Expectations", token.to_string(tok), state.line, state.column, )) } } fn parse_expects_block( state: ParserState, leading_comments: List(Comment), ) -> Result(#(ExpectsBlock, List(Comment), ParserState), ParserError) { use state <- result.try(expect( state, token.KeywordExpectations, "Expectations", )) use state <- result.try(expect(state, token.KeywordFor, "for")) use #(blueprint, state) <- result.try(parse_string_literal(state)) use #(items, trailing, state) <- result.try(parse_expect_items(state)) Ok(#(ast.ExpectsBlock(blueprint:, items:, leading_comments:), trailing, state)) } fn parse_expect_items( state: ParserState, ) -> Result(#(List(ExpectItem), List(Comment), ParserState), ParserError) { let #(pending, state) = consume_comments(state) parse_expect_items_loop(state, [], pending) } fn parse_expect_items_loop( state: ParserState, acc: List(ExpectItem), pending: List(Comment), ) -> Result(#(List(ExpectItem), List(Comment), ParserState), ParserError) { case peek(state) { token.SymbolStar -> { use #(item, state) <- result.try(parse_expect_item(state, pending)) let #(next_pending, state) = consume_comments(state) parse_expect_items_loop(state, [item, ..acc], next_pending) } _ -> Ok(#(list.reverse(acc), pending, state)) } } 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)) } } } // ============================================================================= // 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)) token.Identifier(name) -> { let state = advance(state) parse_extends_list_loop(state, [name]) } tok -> Error(parser_error.UnexpectedToken( "identifier", token.to_string(tok), state.line, state.column, )) } } fn parse_extends_list_loop( state: ParserState, acc: List(String), ) -> Result(#(List(String), ParserState), ParserError) { case peek(state) { token.SymbolComma -> { let state = advance(state) case peek(state) { token.Identifier(name) -> { let state = advance(state) parse_extends_list_loop(state, [name, ..acc]) } tok -> Error(parser_error.UnexpectedToken( "identifier", token.to_string(tok), state.line, state.column, )) } } _ -> 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 // ============================================================================= fn parse_type( state: ParserState, ) -> Result(#(ParsedType, ParserState), ParserError) { case peek(state) { token.KeywordString -> parse_type_with_refinement(state, StringType) token.KeywordInteger -> parse_type_with_refinement(state, NumericType(Integer)) token.KeywordFloat -> parse_type_with_refinement(state, NumericType(Float)) token.KeywordBoolean -> parse_type_with_refinement(state, Boolean) token.KeywordURL -> parse_type_with_refinement(state, SemanticType(URL)) token.KeywordList -> parse_list_type(state) token.KeywordDict -> parse_dict_type(state) token.KeywordOptional -> parse_optional_type(state) token.KeywordDefaulted -> parse_defaulted_type(state) // Record type (e.g., { numerator: String, denominator: String }) token.SymbolLeftBrace -> parse_record_type(state) // Type alias reference (must start with _, e.g., _env) token.Identifier(name) -> case string.starts_with(name, "_") { True -> { let state = advance(state) Ok(#(ParsedTypeAliasRef(name), state)) } False -> Error(parser_error.UnknownType(name, state.line, state.column)) } tok -> Error(parser_error.UnknownType( token.to_string(tok), state.line, state.column, )) } } fn parse_type_with_refinement( state: ParserState, primitive: PrimitiveTypes, ) -> Result(#(ParsedType, ParserState), ParserError) { let state = advance(state) case peek(state) { token.SymbolLeftBrace -> { use #(refinement, state) <- result.try(parse_refinement(state, primitive)) Ok(#(ParsedRefinement(refinement), state)) } _ -> Ok(#(ParsedPrimitive(primitive), state)) } } /// 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 peek(state) { token.KeywordString -> { let state = advance(state) Ok(#(ParsedPrimitive(StringType), state)) } token.KeywordInteger -> { let state = advance(state) Ok(#(ParsedPrimitive(NumericType(Integer)), state)) } token.KeywordFloat -> { let state = advance(state) Ok(#(ParsedPrimitive(NumericType(Float)), state)) } token.KeywordBoolean -> { let state = advance(state) Ok(#(ParsedPrimitive(Boolean), state)) } token.KeywordURL -> { let state = advance(state) Ok(#(ParsedPrimitive(SemanticType(URL)), state)) } token.KeywordList -> parse_list_type(state) token.KeywordDict -> parse_dict_type(state) // Record type (e.g., { numerator: String, denominator: String }) token.SymbolLeftBrace -> parse_record_type(state) // Type alias reference (must start with _, e.g., _env) token.Identifier(name) -> case string.starts_with(name, "_") { True -> { let state = advance(state) Ok(#(ParsedTypeAliasRef(name), state)) } False -> Error(parser_error.UnknownType(name, state.line, state.column)) } 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 -> { let state = advance(state) Ok(#(ParsedPrimitive(StringType), state)) } // Type alias reference (must start with _, e.g., _env) - must resolve to a String-based type token.Identifier(name) -> case string.starts_with(name, "_") { True -> { let state = advance(state) Ok(#(ParsedTypeAliasRef(name), state)) } False -> Error(parser_error.UnknownType(name, state.line, state.column)) } 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_defaulted_refinement( state, defaulted, )) Ok(#(ParsedRefinement(refinement), state)) } _ -> Ok(#(defaulted, state)) } } /// Parse refinement on a Defaulted type: { x | x in { ... } } or { x | x in ( ... ) } fn parse_defaulted_refinement( state: ParserState, defaulted: 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_defaulted_refinement_body( state, defaulted, )) use state <- result.try(expect(state, token.SymbolRightBrace, "}")) Ok(#(refinement, state)) } fn parse_defaulted_refinement_body( state: ParserState, defaulted: ParsedType, ) -> Result(#(RefinementTypes(ParsedType), ParserState), ParserError) { let primitive = extract_primitive_from_parsed(defaulted) 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, "}")) use _ <- result.try(case primitive { Ok(prim) -> validate_oneof_literals(values, prim, state) Error(_) -> Ok(Nil) }) let string_values = list.map(values, literal_to_string) Ok(#(OneOf(defaulted, 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, ")")) use _ <- result.try(case primitive { Ok(prim) -> validate_oneof_literals([min, max], prim, state) Error(_) -> Ok(Nil) }) Ok(#( InclusiveRange( defaulted, literal_to_string(min), literal_to_string(max), ), state, )) } tok -> Error(parser_error.UnexpectedToken( "{ or (", token.to_string(tok), state.line, state.column, )) } } // ============================================================================= // REFINEMENTS // ============================================================================= fn parse_refinement( state: ParserState, primitive: PrimitiveTypes, ) -> 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, primitive)) 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, primitive: PrimitiveTypes, ) -> 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, "}")) use _ <- result.try(validate_oneof_literals(values, primitive, state)) let string_values = list.map(values, literal_to_string) Ok(#( OneOf(ParsedPrimitive(primitive), 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, ")")) use _ <- result.try(validate_oneof_literals([min, max], primitive, state)) Ok(#( InclusiveRange( ParsedPrimitive(primitive), 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.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)) parse_literal_list_loop(state, [first]) } } } fn parse_literal_list_loop( state: ParserState, acc: List(Literal), ) -> Result(#(List(Literal), ParserState), ParserError) { case peek(state) { token.SymbolComma -> { let state = advance(state) use #(literal, state) <- result.try(parse_literal(state)) parse_literal_list_loop(state, [literal, ..acc]) } _ -> Ok(#(list.reverse(acc), state)) } } 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. 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.LiteralTrue -> "True" ast.LiteralFalse -> "False" ast.LiteralList(_) -> "[]" ast.LiteralStruct(_, _) -> "{}" } } /// Validates that all literals in a OneOf set match the declared primitive type. fn validate_oneof_literals( literals: List(Literal), primitive: PrimitiveTypes, state: ParserState, ) -> Result(Nil, ParserError) { case literals { [] -> Ok(Nil) [first, ..rest] -> { case literal_matches_primitive(first, primitive) { True -> validate_oneof_literals(rest, primitive, state) False -> Error(parser_error.InvalidRefinement( "value '" <> literal_to_string(first) <> "' is not a valid " <> types.primitive_type_to_string(primitive) <> " literal", state.line, state.column, )) } } } } /// Checks if a literal value is compatible with the declared primitive type. fn literal_matches_primitive( literal: Literal, primitive: PrimitiveTypes, ) -> Bool { case primitive, literal { StringType, ast.LiteralString(_) -> True NumericType(Integer), ast.LiteralInteger(_) -> True NumericType(Float), ast.LiteralFloat(_) -> True Boolean, ast.LiteralTrue -> True Boolean, ast.LiteralFalse -> True SemanticType(_), ast.LiteralString(_) -> True _, _ -> False } } /// Extracts the primitive type from a ParsedType, unwrapping Defaulted modifiers. fn extract_primitive_from_parsed(typ: ParsedType) -> Result(PrimitiveTypes, Nil) { case typ { ParsedPrimitive(primitive) -> Ok(primitive) ParsedModifier(Defaulted(inner, _)) -> extract_primitive_from_parsed(inner) _ -> Error(Nil) } }