···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Block scalar chomping indicators *)
77+88+type t =
99+ | Strip (** Remove final line break and trailing empty lines *)
1010+ | Clip (** Keep final line break, remove trailing empty lines (default) *)
1111+ | Keep (** Keep final line break and trailing empty lines *)
1212+1313+val to_string : t -> string
1414+(** Convert chomping mode to string *)
1515+1616+val pp : Format.formatter -> t -> unit
1717+(** Pretty-print a chomping mode *)
1818+1919+val of_char : char -> t option
2020+(** Parse chomping indicator from character *)
2121+2222+val to_char : t -> char option
2323+(** Convert chomping mode to indicator character (None for Clip) *)
2424+2525+val equal : t -> t -> bool
2626+(** Test equality of two chomping modes *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML parser events *)
77+88+type t =
99+ | Stream_start of { encoding : Encoding.t }
1010+ | Stream_end
1111+ | Document_start of {
1212+ version : (int * int) option;
1313+ implicit : bool;
1414+ }
1515+ | Document_end of { implicit : bool }
1616+ | Alias of { anchor : string }
1717+ | Scalar of {
1818+ anchor : string option;
1919+ tag : string option;
2020+ value : string;
2121+ plain_implicit : bool;
2222+ quoted_implicit : bool;
2323+ style : Scalar_style.t;
2424+ }
2525+ | Sequence_start of {
2626+ anchor : string option;
2727+ tag : string option;
2828+ implicit : bool;
2929+ style : Layout_style.t;
3030+ }
3131+ | Sequence_end
3232+ | Mapping_start of {
3333+ anchor : string option;
3434+ tag : string option;
3535+ implicit : bool;
3636+ style : Layout_style.t;
3737+ }
3838+ | Mapping_end
3939+4040+type spanned = {
4141+ event : t;
4242+ span : Span.t;
4343+}
4444+4545+val pp : Format.formatter -> t -> unit
4646+(** Pretty-print an event *)
4747+4848+val pp_spanned : Format.formatter -> spanned -> unit
4949+(** Pretty-print a spanned event *)
+97
lib/input.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Character input source with lookahead, based on Bytes.Reader.t
77+88+ This module wraps a bytesrw [Bytes.Reader.t] to provide character-by-character
99+ access with lookahead for the YAML scanner. *)
1010+1111+(** {2 Re-exported Character Classification} *)
1212+1313+include module type of Char_class
1414+1515+(** {2 Input Type} *)
1616+1717+type t
1818+1919+(** {2 Constructors} *)
2020+2121+val of_reader : ?initial_position:Position.t -> Bytesrw.Bytes.Reader.t -> t
2222+(** Create input from a Bytes.Reader.t *)
2323+2424+val of_string : string -> t
2525+(** Create input from a string *)
2626+2727+(** {2 Position and State} *)
2828+2929+val position : t -> Position.t
3030+(** Get current position *)
3131+3232+val is_eof : t -> bool
3333+(** Check if at end of input *)
3434+3535+val mark : t -> Position.t
3636+(** Mark current position for span creation *)
3737+3838+(** {2 Lookahead} *)
3939+4040+val peek : t -> char option
4141+(** Peek at current character without advancing *)
4242+4343+val peek_exn : t -> char
4444+(** Peek at current character, raising on EOF *)
4545+4646+val peek_nth : t -> int -> char option
4747+(** Peek at nth character (0-indexed from current position) *)
4848+4949+val peek_string : t -> int -> string
5050+(** Peek at up to n characters as a string *)
5151+5252+val peek_back : t -> char option
5353+(** Get the character before the current position *)
5454+5555+(** {2 Consumption} *)
5656+5757+val next : t -> char option
5858+(** Consume and return next character *)
5959+6060+val next_exn : t -> char
6161+(** Consume and return next character, raising on EOF *)
6262+6363+val skip : t -> int -> unit
6464+(** Skip n characters *)
6565+6666+val skip_while : t -> (char -> bool) -> unit
6767+(** Skip characters while predicate holds *)
6868+6969+val consume_break : t -> unit
7070+(** Consume line break, handling \r\n as single break *)
7171+7272+(** {2 Predicates} *)
7373+7474+val next_is : (char -> bool) -> t -> bool
7575+(** Check if next char satisfies predicate *)
7676+7777+val next_is_break : t -> bool
7878+val next_is_blank : t -> bool
7979+val next_is_whitespace : t -> bool
8080+val next_is_digit : t -> bool
8181+val next_is_hex : t -> bool
8282+val next_is_alpha : t -> bool
8383+val next_is_indicator : t -> bool
8484+8585+val at_document_boundary : t -> bool
8686+(** Check if at document boundary (--- or ...) *)
8787+8888+(** {2 Utilities} *)
8989+9090+val remaining : t -> string
9191+(** Get remaining content from current position *)
9292+9393+val source : t -> string
9494+(** Get a sample of the source for encoding detection *)
9595+9696+val byte_pos : t -> int
9797+(** Get the byte position in the underlying stream *)
+24
lib/layout_style.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Collection layout styles *)
77+88+type t = [
99+ | `Any (** Let emitter choose *)
1010+ | `Block (** Indentation-based *)
1111+ | `Flow (** Inline with brackets *)
1212+]
1313+1414+val to_string : t -> string
1515+(** Convert style to string representation *)
1616+1717+val pp : Format.formatter -> t -> unit
1818+(** Pretty-print a style *)
1919+2020+val equal : t -> t -> bool
2121+(** Test equality of two styles *)
2222+2323+val compare : t -> t -> int
2424+(** Compare two styles *)
+58-87
lib/loader.ml
···132132 pending_key = None;
133133 } :: rest)
134134135135+(** Internal: parse all documents from a parser *)
136136+let parse_all_documents parser =
137137+ let state = create_state () in
138138+ Parser.iter (process_event state) parser;
139139+ List.rev state.documents
140140+141141+(** Internal: extract single document or raise *)
142142+let single_document_or_error docs ~empty =
143143+ match docs with
144144+ | [] -> empty
145145+ | [doc] -> doc
146146+ | _ -> Error.raise Multiple_documents
147147+135148(** Load single document as Value.
136149137150 @param resolve_aliases Whether to resolve aliases (default true)
···143156 ?(max_nodes = Yaml.default_max_alias_nodes)
144157 ?(max_depth = Yaml.default_max_alias_depth)
145158 s =
146146- let parser = Parser.of_string s in
147147- let state = create_state () in
148148- Parser.iter (process_event state) parser;
149149- match state.documents with
150150- | [] -> `Null
151151- | [doc] ->
152152- (match Document.root doc with
153153- | None -> `Null
154154- | Some yaml ->
155155- Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml)
156156- | _ -> Error.raise Multiple_documents
159159+ let docs = parse_all_documents (Parser.of_string s) in
160160+ let doc = single_document_or_error docs ~empty:(Document.make None) in
161161+ match Document.root doc with
162162+ | None -> `Null
163163+ | Some yaml ->
164164+ Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml
157165158166(** Load single document as Yaml.
159167···166174 ?(max_nodes = Yaml.default_max_alias_nodes)
167175 ?(max_depth = Yaml.default_max_alias_depth)
168176 s =
169169- let parser = Parser.of_string s in
170170- let state = create_state () in
171171- Parser.iter (process_event state) parser;
172172- match state.documents with
173173- | [] -> `Scalar (Scalar.make "")
174174- | [doc] ->
175175- (match Document.root doc with
176176- | None -> `Scalar (Scalar.make "")
177177- | Some yaml ->
178178- if resolve_aliases then
179179- Yaml.resolve_aliases ~max_nodes ~max_depth yaml
180180- else
181181- yaml)
182182- | _ -> Error.raise Multiple_documents
177177+ let docs = parse_all_documents (Parser.of_string s) in
178178+ let doc = single_document_or_error docs ~empty:(Document.make None) in
179179+ match Document.root doc with
180180+ | None -> `Scalar (Scalar.make "")
181181+ | Some yaml ->
182182+ if resolve_aliases then
183183+ Yaml.resolve_aliases ~max_nodes ~max_depth yaml
184184+ else
185185+ yaml
183186184187(** Load all documents *)
185188let documents_of_string s =
186186- let parser = Parser.of_string s in
187187- let state = create_state () in
188188- Parser.iter (process_event state) parser;
189189- List.rev state.documents
189189+ parse_all_documents (Parser.of_string s)
190190191191(** {2 Reader-based loading} *)
192192···201201 ?(max_nodes = Yaml.default_max_alias_nodes)
202202 ?(max_depth = Yaml.default_max_alias_depth)
203203 reader =
204204- let parser = Parser.of_reader reader in
205205- let state = create_state () in
206206- Parser.iter (process_event state) parser;
207207- match state.documents with
208208- | [] -> `Null
209209- | [doc] ->
210210- (match Document.root doc with
211211- | None -> `Null
212212- | Some yaml ->
213213- Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml)
214214- | _ -> Error.raise Multiple_documents
204204+ let docs = parse_all_documents (Parser.of_reader reader) in
205205+ let doc = single_document_or_error docs ~empty:(Document.make None) in
206206+ match Document.root doc with
207207+ | None -> `Null
208208+ | Some yaml ->
209209+ Yaml.to_value ~resolve_aliases_first:resolve_aliases ~max_nodes ~max_depth yaml
215210216211(** Load single document as Yaml from a Bytes.Reader.
217212···224219 ?(max_nodes = Yaml.default_max_alias_nodes)
225220 ?(max_depth = Yaml.default_max_alias_depth)
226221 reader =
227227- let parser = Parser.of_reader reader in
228228- let state = create_state () in
229229- Parser.iter (process_event state) parser;
230230- match state.documents with
231231- | [] -> `Scalar (Scalar.make "")
232232- | [doc] ->
233233- (match Document.root doc with
234234- | None -> `Scalar (Scalar.make "")
235235- | Some yaml ->
236236- if resolve_aliases then
237237- Yaml.resolve_aliases ~max_nodes ~max_depth yaml
238238- else
239239- yaml)
240240- | _ -> Error.raise Multiple_documents
222222+ let docs = parse_all_documents (Parser.of_reader reader) in
223223+ let doc = single_document_or_error docs ~empty:(Document.make None) in
224224+ match Document.root doc with
225225+ | None -> `Scalar (Scalar.make "")
226226+ | Some yaml ->
227227+ if resolve_aliases then
228228+ Yaml.resolve_aliases ~max_nodes ~max_depth yaml
229229+ else
230230+ yaml
241231242232(** Load all documents from a Bytes.Reader *)
243233let documents_of_reader reader =
244244- let parser = Parser.of_reader reader in
245245- let state = create_state () in
246246- Parser.iter (process_event state) parser;
247247- List.rev state.documents
234234+ parse_all_documents (Parser.of_reader reader)
235235+236236+(** {2 Parser-function based loading}
248237249249-(** Generic document loader - extracts common pattern from load_* functions *)
250250-let load_generic extract parser =
238238+ These functions accept a [unit -> Event.spanned option] function
239239+ instead of a [Parser.t], allowing them to work with any event source
240240+ (e.g., streaming parsers). *)
241241+242242+(** Generic document loader using event source function *)
243243+let load_generic_fn extract next_event =
251244 let state = create_state () in
252245 let rec loop () =
253253- match Parser.next parser with
246246+ match next_event () with
254247 | None -> None
255248 | Some ev ->
256249 process_event state ev;
···265258 | _ -> loop ()
266259 in
267260 loop ()
261261+262262+(** Generic document loader - extracts common pattern from load_* functions *)
263263+let load_generic extract parser =
264264+ load_generic_fn extract (fun () -> Parser.next parser)
268265269266(** Load single Value from parser.
270267···311308 | Some doc -> loop (f acc doc)
312309 in
313310 loop init
314314-315315-(** {2 Parser-function based loading}
316316-317317- These functions accept a [unit -> Event.spanned option] function
318318- instead of a [Parser.t], allowing them to work with any event source
319319- (e.g., streaming parsers). *)
320320-321321-(** Generic document loader using event source function *)
322322-let load_generic_fn extract next_event =
323323- let state = create_state () in
324324- let rec loop () =
325325- match next_event () with
326326- | None -> None
327327- | Some ev ->
328328- process_event state ev;
329329- match ev.event with
330330- | Event.Document_end _ ->
331331- (match state.documents with
332332- | doc :: _ ->
333333- state.documents <- [];
334334- Some (extract doc)
335335- | [] -> None)
336336- | Event.Stream_end -> None
337337- | _ -> loop ()
338338- in
339339- loop ()
340311341312(** Load single Value from event source.
342313
+104
lib/loader.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Loader - converts parser events to YAML data structures *)
77+88+(** {1 String-based loading} *)
99+1010+val value_of_string :
1111+ ?resolve_aliases:bool ->
1212+ ?max_nodes:int ->
1313+ ?max_depth:int ->
1414+ string -> Value.t
1515+(** Load single document as Value.
1616+1717+ @param resolve_aliases Whether to resolve aliases (default true)
1818+ @param max_nodes Maximum nodes during alias expansion (default 10M)
1919+ @param max_depth Maximum alias nesting depth (default 100) *)
2020+2121+val yaml_of_string :
2222+ ?resolve_aliases:bool ->
2323+ ?max_nodes:int ->
2424+ ?max_depth:int ->
2525+ string -> Yaml.t
2626+(** Load single document as Yaml.
2727+2828+ @param resolve_aliases Whether to resolve aliases (default false)
2929+ @param max_nodes Maximum nodes during alias expansion (default 10M)
3030+ @param max_depth Maximum alias nesting depth (default 100) *)
3131+3232+val documents_of_string : string -> Document.t list
3333+(** Load all documents from a string *)
3434+3535+(** {1 Reader-based loading} *)
3636+3737+val value_of_reader :
3838+ ?resolve_aliases:bool ->
3939+ ?max_nodes:int ->
4040+ ?max_depth:int ->
4141+ Bytesrw.Bytes.Reader.t -> Value.t
4242+(** Load single document as Value from a Bytes.Reader *)
4343+4444+val yaml_of_reader :
4545+ ?resolve_aliases:bool ->
4646+ ?max_nodes:int ->
4747+ ?max_depth:int ->
4848+ Bytesrw.Bytes.Reader.t -> Yaml.t
4949+(** Load single document as Yaml from a Bytes.Reader *)
5050+5151+val documents_of_reader : Bytesrw.Bytes.Reader.t -> Document.t list
5252+(** Load all documents from a Bytes.Reader *)
5353+5454+(** {1 Parser-based loading} *)
5555+5656+val load_value :
5757+ ?resolve_aliases:bool ->
5858+ ?max_nodes:int ->
5959+ ?max_depth:int ->
6060+ Parser.t -> Value.t option
6161+(** Load single Value from parser *)
6262+6363+val load_yaml : Parser.t -> Yaml.t option
6464+(** Load single Yaml from parser *)
6565+6666+val load_document : Parser.t -> Document.t option
6767+(** Load single Document from parser *)
6868+6969+val iter_documents : (Document.t -> unit) -> Parser.t -> unit
7070+(** Iterate over documents from parser *)
7171+7272+val fold_documents : ('a -> Document.t -> 'a) -> 'a -> Parser.t -> 'a
7373+(** Fold over documents from parser *)
7474+7575+(** {1 Event function-based loading}
7676+7777+ These functions accept a [unit -> Event.spanned option] function
7878+ instead of a [Parser.t], allowing them to work with any event source. *)
7979+8080+val value_of_parser :
8181+ ?resolve_aliases:bool ->
8282+ ?max_nodes:int ->
8383+ ?max_depth:int ->
8484+ (unit -> Event.spanned option) -> Value.t
8585+(** Load single Value from event source function *)
8686+8787+val yaml_of_parser :
8888+ ?resolve_aliases:bool ->
8989+ ?max_nodes:int ->
9090+ ?max_depth:int ->
9191+ (unit -> Event.spanned option) -> Yaml.t
9292+(** Load single Yaml from event source function *)
9393+9494+val document_of_parser : (unit -> Event.spanned option) -> Document.t option
9595+(** Load single Document from event source function *)
9696+9797+val documents_of_parser : (unit -> Event.spanned option) -> Document.t list
9898+(** Load all documents from event source function *)
9999+100100+val iter_documents_parser : (Document.t -> unit) -> (unit -> Event.spanned option) -> unit
101101+(** Iterate over documents from event source function *)
102102+103103+val fold_documents_parser : ('a -> Document.t -> 'a) -> 'a -> (unit -> Event.spanned option) -> 'a
104104+(** Fold over documents from event source function *)
+2-6
lib/mapping.ml
···58585959let pp pp_key pp_val fmt t =
6060 Format.fprintf fmt "@[<hv 2>mapping(@,";
6161- (match t.anchor with
6262- | Some a -> Format.fprintf fmt "anchor=%s,@ " a
6363- | None -> ());
6464- (match t.tag with
6565- | Some tag -> Format.fprintf fmt "tag=%s,@ " tag
6666- | None -> ());
6161+ Option.iter (Format.fprintf fmt "anchor=%s,@ ") t.anchor;
6262+ Option.iter (Format.fprintf fmt "tag=%s,@ ") t.tag;
6763 Format.fprintf fmt "style=%a,@ " Layout_style.pp t.style;
6864 Format.fprintf fmt "members={@,";
6965 List.iteri (fun i (k, v) ->
+54
lib/mapping.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML mapping (object) values with metadata *)
77+88+type ('k, 'v) t
99+1010+val make :
1111+ ?anchor:string ->
1212+ ?tag:string ->
1313+ ?implicit:bool ->
1414+ ?style:Layout_style.t ->
1515+ ('k * 'v) list -> ('k, 'v) t
1616+(** Create a mapping *)
1717+1818+(** {2 Accessors} *)
1919+2020+val members : ('k, 'v) t -> ('k * 'v) list
2121+val anchor : ('k, 'v) t -> string option
2222+val tag : ('k, 'v) t -> string option
2323+val implicit : ('k, 'v) t -> bool
2424+val style : ('k, 'v) t -> Layout_style.t
2525+2626+(** {2 Modifiers} *)
2727+2828+val with_anchor : string -> ('k, 'v) t -> ('k, 'v) t
2929+val with_tag : string -> ('k, 'v) t -> ('k, 'v) t
3030+val with_style : Layout_style.t -> ('k, 'v) t -> ('k, 'v) t
3131+3232+(** {2 Operations} *)
3333+3434+val map_keys : ('k -> 'k2) -> ('k, 'v) t -> ('k2, 'v) t
3535+val map_values : ('v -> 'v2) -> ('k, 'v) t -> ('k, 'v2) t
3636+val map : ('k -> 'v -> 'k2 * 'v2) -> ('k, 'v) t -> ('k2, 'v2) t
3737+val length : ('k, 'v) t -> int
3838+val is_empty : ('k, 'v) t -> bool
3939+val find : ('k -> bool) -> ('k, 'v) t -> 'v option
4040+val find_key : ('k -> bool) -> ('k, 'v) t -> ('k * 'v) option
4141+val mem : ('k -> bool) -> ('k, 'v) t -> bool
4242+val keys : ('k, 'v) t -> 'k list
4343+val values : ('k, 'v) t -> 'v list
4444+val iter : ('k -> 'v -> unit) -> ('k, 'v) t -> unit
4545+val fold : ('a -> 'k -> 'v -> 'a) -> 'a -> ('k, 'v) t -> 'a
4646+4747+(** {2 Comparison} *)
4848+4949+val pp :
5050+ (Format.formatter -> 'k -> unit) ->
5151+ (Format.formatter -> 'v -> unit) ->
5252+ Format.formatter -> ('k, 'v) t -> unit
5353+val equal : ('k -> 'k -> bool) -> ('v -> 'v -> bool) -> ('k, 'v) t -> ('k, 'v) t -> bool
5454+val compare : ('k -> 'k -> int) -> ('v -> 'v -> int) -> ('k, 'v) t -> ('k, 'v) t -> int
+1-5
lib/parser.ml
···9090let skip_token t =
9191 t.current_token <- None
92929393-(** Check if current token matches *)
9393+(** Check if current token matches predicate *)
9494let check t pred =
9595 match peek_token t with
9696 | Some tok -> pred tok.token
9797 | None -> false
9898-9999-(** Check for specific token *)
100100-let check_token t token_match =
101101- check t token_match
1029810399(** Push state onto stack *)
104100let push_state t s =
+41
lib/parser.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML parser - converts tokens to semantic events via state machine *)
77+88+type t
99+1010+(** {2 Constructors} *)
1111+1212+val of_string : string -> t
1313+(** Create parser from a string *)
1414+1515+val of_scanner : Scanner.t -> t
1616+(** Create parser from a scanner *)
1717+1818+val of_input : Input.t -> t
1919+(** Create parser from an input source *)
2020+2121+val of_reader : Bytesrw.Bytes.Reader.t -> t
2222+(** Create parser from a Bytes.Reader *)
2323+2424+(** {2 Event Access} *)
2525+2626+val next : t -> Event.spanned option
2727+(** Get next event *)
2828+2929+val peek : t -> Event.spanned option
3030+(** Peek at next event without consuming *)
3131+3232+(** {2 Iteration} *)
3333+3434+val iter : (Event.spanned -> unit) -> t -> unit
3535+(** Iterate over all events *)
3636+3737+val fold : ('a -> Event.spanned -> 'a) -> 'a -> t -> 'a
3838+(** Fold over all events *)
3939+4040+val to_list : t -> Event.spanned list
4141+(** Convert to list of events *)
+42
lib/position.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Position tracking for source locations *)
77+88+type t = {
99+ index : int; (** Byte offset from start *)
1010+ line : int; (** 1-indexed line number *)
1111+ column : int; (** 1-indexed column number *)
1212+}
1313+1414+val initial : t
1515+(** Initial position (index=0, line=1, column=1) *)
1616+1717+val advance_byte : t -> t
1818+(** Advance by one byte (increments index and column) *)
1919+2020+val advance_line : t -> t
2121+(** Advance to next line (increments index and line, resets column to 1) *)
2222+2323+val advance_char : char -> t -> t
2424+(** Advance by one character, handling newlines appropriately *)
2525+2626+val advance_utf8 : Uchar.t -> t -> t
2727+(** Advance by one Unicode character, handling newlines and multi-byte characters *)
2828+2929+val advance_bytes : int -> t -> t
3030+(** Advance by n bytes *)
3131+3232+val pp : Format.formatter -> t -> unit
3333+(** Pretty-print a position *)
3434+3535+val to_string : t -> string
3636+(** Convert position to string *)
3737+3838+val compare : t -> t -> int
3939+(** Compare two positions by index *)
4040+4141+val equal : t -> t -> bool
4242+(** Test equality of two positions *)
+22
lib/quoting.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML scalar quoting detection *)
77+88+val needs_quoting : string -> bool
99+(** Check if a string value needs quoting in YAML output.
1010+ Returns true if the string:
1111+ - Is empty
1212+ - Starts with an indicator character
1313+ - Is a reserved word (null, true, false, yes, no, etc.)
1414+ - Contains characters that would be ambiguous
1515+ - Looks like a number *)
1616+1717+val needs_double_quotes : string -> bool
1818+(** Check if a string requires double quotes (vs single quotes).
1919+ Returns true if the string contains characters that need escape sequences. *)
2020+2121+val choose_style : string -> [> `Plain | `Single_quoted | `Double_quoted ]
2222+(** Choose the appropriate quoting style for a string value *)
+2-6
lib/scalar.ml
···36363737let pp fmt t =
3838 Format.fprintf fmt "scalar(%S" t.value;
3939- (match t.anchor with
4040- | Some a -> Format.fprintf fmt ", anchor=%s" a
4141- | None -> ());
4242- (match t.tag with
4343- | Some tag -> Format.fprintf fmt ", tag=%s" tag
4444- | None -> ());
3939+ Option.iter (Format.fprintf fmt ", anchor=%s") t.anchor;
4040+ Option.iter (Format.fprintf fmt ", tag=%s") t.tag;
4541 Format.fprintf fmt ", style=%a)" Scalar_style.pp t.style
46424743let equal a b =
+38
lib/scalar.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML scalar values with metadata *)
77+88+type t
99+1010+val make :
1111+ ?anchor:string ->
1212+ ?tag:string ->
1313+ ?plain_implicit:bool ->
1414+ ?quoted_implicit:bool ->
1515+ ?style:Scalar_style.t ->
1616+ string -> t
1717+(** Create a scalar value *)
1818+1919+(** {2 Accessors} *)
2020+2121+val value : t -> string
2222+val anchor : t -> string option
2323+val tag : t -> string option
2424+val style : t -> Scalar_style.t
2525+val plain_implicit : t -> bool
2626+val quoted_implicit : t -> bool
2727+2828+(** {2 Modifiers} *)
2929+3030+val with_anchor : string -> t -> t
3131+val with_tag : string -> t -> t
3232+val with_style : Scalar_style.t -> t -> t
3333+3434+(** {2 Comparison} *)
3535+3636+val pp : Format.formatter -> t -> unit
3737+val equal : t -> t -> bool
3838+val compare : t -> t -> int
+27
lib/scalar_style.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Scalar formatting styles *)
77+88+type t = [
99+ | `Any (** Let emitter choose *)
1010+ | `Plain (** Unquoted: foo *)
1111+ | `Single_quoted (** 'foo' *)
1212+ | `Double_quoted (** "foo" *)
1313+ | `Literal (** | block *)
1414+ | `Folded (** > block *)
1515+]
1616+1717+val to_string : t -> string
1818+(** Convert style to string representation *)
1919+2020+val pp : Format.formatter -> t -> unit
2121+(** Pretty-print a style *)
2222+2323+val equal : t -> t -> bool
2424+(** Test equality of two styles *)
2525+2626+val compare : t -> t -> int
2727+(** Compare two styles *)
+14-25
lib/scanner.ml
···14311431 emit t span Token.Value;
14321432 t.pending_value <- false (* We've emitted a VALUE, no longer pending *)
1433143314341434-and fetch_alias t =
14341434+and fetch_anchor_or_alias t ~is_alias =
14351435 save_simple_key t;
14361436 t.allow_simple_key <- false;
14371437 t.document_has_content <- true;
14381438 let start = Input.mark t.input in
14391439- ignore (Input.next t.input); (* consume * *)
14391439+ ignore (Input.next t.input); (* consume * or & *)
14401440 let name, span = scan_anchor_alias t in
14411441 let span = Span.make ~start ~stop:span.stop in
14421442- emit t span (Token.Alias name)
14421442+ let token = if is_alias then Token.Alias name else Token.Anchor name in
14431443+ emit t span token
1443144414441444-and fetch_anchor t =
14451445- save_simple_key t;
14461446- t.allow_simple_key <- false;
14471447- t.document_has_content <- true;
14481448- let start = Input.mark t.input in
14491449- ignore (Input.next t.input); (* consume & *)
14501450- let name, span = scan_anchor_alias t in
14511451- let span = Span.make ~start ~stop:span.stop in
14521452- emit t span (Token.Anchor name)
14451445+and fetch_alias t = fetch_anchor_or_alias t ~is_alias:true
14461446+and fetch_anchor t = fetch_anchor_or_alias t ~is_alias:false
1453144714541448and fetch_tag t =
14551449 save_simple_key t;
···14651459 let value, style, span = scan_block_scalar t literal in
14661460 emit t span (Token.Scalar { style; value })
1467146114681468-and fetch_single_quoted t =
14621462+and fetch_quoted t ~double =
14691463 save_simple_key t;
14701464 t.allow_simple_key <- false;
14711465 t.document_has_content <- true;
14721472- let value, span = scan_single_quoted t in
14661466+ let value, span =
14671467+ if double then scan_double_quoted t else scan_single_quoted t
14681468+ in
14731469 (* Allow adjacent values after quoted scalars in flow context (for JSON compatibility) *)
14741470 skip_to_next_token t;
14751471 if t.flow_level > 0 then
14761472 t.adjacent_value_allowed_at <- Some (Input.position t.input);
14771477- emit t span (Token.Scalar { style = `Single_quoted; value })
14731473+ let style = if double then `Double_quoted else `Single_quoted in
14741474+ emit t span (Token.Scalar { style; value })
1478147514791479-and fetch_double_quoted t =
14801480- save_simple_key t;
14811481- t.allow_simple_key <- false;
14821482- t.document_has_content <- true;
14831483- let value, span = scan_double_quoted t in
14841484- (* Allow adjacent values after quoted scalars in flow context (for JSON compatibility) *)
14851485- skip_to_next_token t;
14861486- if t.flow_level > 0 then
14871487- t.adjacent_value_allowed_at <- Some (Input.position t.input);
14881488- emit t span (Token.Scalar { style = `Double_quoted; value })
14761476+and fetch_single_quoted t = fetch_quoted t ~double:false
14771477+and fetch_double_quoted t = fetch_quoted t ~double:true
1489147814901479and can_start_plain t =
14911480 (* Check if - ? : can start a plain scalar *)
+43
lib/scanner.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML tokenizer/scanner with lookahead for ambiguity resolution *)
77+88+type t
99+1010+(** {2 Constructors} *)
1111+1212+val of_string : string -> t
1313+(** Create scanner from a string *)
1414+1515+val of_input : Input.t -> t
1616+(** Create scanner from an input source *)
1717+1818+val of_reader : Bytesrw.Bytes.Reader.t -> t
1919+(** Create scanner from a Bytes.Reader *)
2020+2121+(** {2 Position} *)
2222+2323+val position : t -> Position.t
2424+(** Get current position in input *)
2525+2626+(** {2 Token Access} *)
2727+2828+val next : t -> Token.spanned option
2929+(** Get next token *)
3030+3131+val peek : t -> Token.spanned option
3232+(** Peek at next token without consuming *)
3333+3434+(** {2 Iteration} *)
3535+3636+val iter : (Token.spanned -> unit) -> t -> unit
3737+(** Iterate over all tokens *)
3838+3939+val fold : ('a -> Token.spanned -> 'a) -> 'a -> t -> 'a
4040+(** Fold over all tokens *)
4141+4242+val to_list : t -> Token.spanned list
4343+(** Convert to list of tokens *)
+2-6
lib/sequence.ml
···47474848let pp pp_elem fmt t =
4949 Format.fprintf fmt "@[<hv 2>sequence(@,";
5050- (match t.anchor with
5151- | Some a -> Format.fprintf fmt "anchor=%s,@ " a
5252- | None -> ());
5353- (match t.tag with
5454- | Some tag -> Format.fprintf fmt "tag=%s,@ " tag
5555- | None -> ());
5050+ Option.iter (Format.fprintf fmt "anchor=%s,@ ") t.anchor;
5151+ Option.iter (Format.fprintf fmt "tag=%s,@ ") t.tag;
5652 Format.fprintf fmt "style=%a,@ " Layout_style.pp t.style;
5753 Format.fprintf fmt "members=[@,%a@]@,)"
5854 (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ",@ ") pp_elem)
+46
lib/sequence.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML sequence (array) values with metadata *)
77+88+type 'a t
99+1010+val make :
1111+ ?anchor:string ->
1212+ ?tag:string ->
1313+ ?implicit:bool ->
1414+ ?style:Layout_style.t ->
1515+ 'a list -> 'a t
1616+(** Create a sequence *)
1717+1818+(** {2 Accessors} *)
1919+2020+val members : 'a t -> 'a list
2121+val anchor : 'a t -> string option
2222+val tag : 'a t -> string option
2323+val implicit : 'a t -> bool
2424+val style : 'a t -> Layout_style.t
2525+2626+(** {2 Modifiers} *)
2727+2828+val with_anchor : string -> 'a t -> 'a t
2929+val with_tag : string -> 'a t -> 'a t
3030+val with_style : Layout_style.t -> 'a t -> 'a t
3131+3232+(** {2 Operations} *)
3333+3434+val map : ('a -> 'b) -> 'a t -> 'b t
3535+val length : 'a t -> int
3636+val is_empty : 'a t -> bool
3737+val nth : 'a t -> int -> 'a
3838+val nth_opt : 'a t -> int -> 'a option
3939+val iter : ('a -> unit) -> 'a t -> unit
4040+val fold : ('b -> 'a -> 'b) -> 'b -> 'a t -> 'b
4141+4242+(** {2 Comparison} *)
4343+4444+val pp : (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit
4545+val equal : ('a -> 'a -> bool) -> 'a t -> 'a t -> bool
4646+val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int
+10-20
lib/serialize.ml
···29293030 | `A seq ->
3131 let members = Sequence.members seq in
3232- let style =
3333- (* Force flow style for empty sequences *)
3434- if members = [] then `Flow
3535- else Sequence.style seq
3636- in
3232+ (* Force flow style for empty sequences *)
3333+ let style = if members = [] then `Flow else Sequence.style seq in
3734 emit (Event.Sequence_start {
3835 anchor = Sequence.anchor seq;
3936 tag = Sequence.tag seq;
···45424643 | `O map ->
4744 let members = Mapping.members map in
4848- let style =
4949- (* Force flow style for empty mappings *)
5050- if members = [] then `Flow
5151- else Mapping.style map
5252- in
4545+ (* Force flow style for empty mappings *)
4646+ let style = if members = [] then `Flow else Mapping.style map in
5347 emit (Event.Mapping_start {
5448 anchor = Mapping.anchor map;
5549 tag = Mapping.tag map;
···111105 })
112106113107 | `A items ->
108108+ (* Force flow style for empty sequences, otherwise use config *)
114109 let style =
115115- (* Force flow style for empty sequences *)
116116- if items = [] then `Flow
117117- else if config.Emitter.layout_style = `Flow then `Flow
118118- else `Block
110110+ if items = [] || config.Emitter.layout_style = `Flow then `Flow else `Block
119111 in
120112 emit (Event.Sequence_start {
121113 anchor = None; tag = None;
···126118 emit Event.Sequence_end
127119128120 | `O pairs ->
121121+ (* Force flow style for empty mappings, otherwise use config *)
129122 let style =
130130- (* Force flow style for empty mappings *)
131131- if pairs = [] then `Flow
132132- else if config.Emitter.layout_style = `Flow then `Flow
133133- else `Block
123123+ if pairs = [] || config.Emitter.layout_style = `Flow then `Flow else `Block
134124 in
135125 emit (Event.Mapping_start {
136126 anchor = None; tag = None;
···339329 emit_yaml_node_impl ~emit:emitter yaml
340330341331(** Emit a complete YAML stream using an emitter function *)
342342-let emit_yaml ~emitter ~config yaml =
332332+let emit_yaml_fn ~emitter ~config yaml =
343333 emitter (Event.Stream_start { encoding = config.Emitter.encoding });
344334 emitter (Event.Document_start { version = None; implicit = true });
345335 emit_yaml_node_fn ~emitter yaml;
···351341 emit_value_node_impl ~emit:emitter ~config value
352342353343(** Emit a complete Value stream using an emitter function *)
354354-let emit_value ~emitter ~config value =
344344+let emit_value_fn ~emitter ~config value =
355345 emitter (Event.Stream_start { encoding = config.Emitter.encoding });
356346 emitter (Event.Document_start { version = None; implicit = true });
357347 emit_value_node_fn ~emitter ~config value;
+133
lib/serialize.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Serialize - high-level serialization to buffers and event streams
77+88+ This module provides functions to convert YAML values to events and strings.
99+ Both {!Emitter.t}-based and function-based emission APIs are provided. *)
1010+1111+(** {1 Emitter.t-based API} *)
1212+1313+val emit_yaml_node : Emitter.t -> Yaml.t -> unit
1414+(** Emit a YAML node to an emitter *)
1515+1616+val emit_yaml : Emitter.t -> Yaml.t -> unit
1717+(** Emit a complete YAML document to an emitter (includes stream/document markers) *)
1818+1919+val emit_value_node : Emitter.t -> Value.t -> unit
2020+(** Emit a Value node to an emitter *)
2121+2222+val emit_value : Emitter.t -> Value.t -> unit
2323+(** Emit a complete Value document to an emitter (includes stream/document markers) *)
2424+2525+val emit_document : ?resolve_aliases:bool -> Emitter.t -> Document.t -> unit
2626+(** Emit a document to an emitter
2727+2828+ @param resolve_aliases Whether to resolve aliases before emission (default true) *)
2929+3030+(** {1 Buffer-based API} *)
3131+3232+val value_to_buffer :
3333+ ?config:Emitter.config ->
3434+ ?buffer:Buffer.t ->
3535+ Value.t -> Buffer.t
3636+(** Serialize a Value to a buffer
3737+3838+ @param config Emitter configuration (default: {!Emitter.default_config})
3939+ @param buffer Optional buffer to append to; creates new one if not provided *)
4040+4141+val yaml_to_buffer :
4242+ ?config:Emitter.config ->
4343+ ?buffer:Buffer.t ->
4444+ Yaml.t -> Buffer.t
4545+(** Serialize a Yaml.t to a buffer *)
4646+4747+val documents_to_buffer :
4848+ ?config:Emitter.config ->
4949+ ?resolve_aliases:bool ->
5050+ ?buffer:Buffer.t ->
5151+ Document.t list -> Buffer.t
5252+(** Serialize documents to a buffer
5353+5454+ @param resolve_aliases Whether to resolve aliases before emission (default true) *)
5555+5656+(** {1 String-based API} *)
5757+5858+val value_to_string : ?config:Emitter.config -> Value.t -> string
5959+(** Serialize a Value to a string *)
6060+6161+val yaml_to_string : ?config:Emitter.config -> Yaml.t -> string
6262+(** Serialize a Yaml.t to a string *)
6363+6464+val documents_to_string :
6565+ ?config:Emitter.config ->
6666+ ?resolve_aliases:bool ->
6767+ Document.t list -> string
6868+(** Serialize documents to a string *)
6969+7070+(** {1 Writer-based API}
7171+7272+ These functions write directly to a bytesrw [Bytes.Writer.t],
7373+ enabling true streaming output without intermediate string allocation. *)
7474+7575+val value_to_writer :
7676+ ?config:Emitter.config ->
7777+ ?eod:bool ->
7878+ Bytesrw.Bytes.Writer.t -> Value.t -> unit
7979+(** Serialize a Value directly to a Bytes.Writer
8080+8181+ @param eod Whether to write end-of-data after serialization (default true) *)
8282+8383+val yaml_to_writer :
8484+ ?config:Emitter.config ->
8585+ ?eod:bool ->
8686+ Bytesrw.Bytes.Writer.t -> Yaml.t -> unit
8787+(** Serialize a Yaml.t directly to a Bytes.Writer *)
8888+8989+val documents_to_writer :
9090+ ?config:Emitter.config ->
9191+ ?resolve_aliases:bool ->
9292+ ?eod:bool ->
9393+ Bytesrw.Bytes.Writer.t -> Document.t list -> unit
9494+(** Serialize documents directly to a Bytes.Writer *)
9595+9696+(** {1 Function-based API}
9797+9898+ These functions accept an emit function [Event.t -> unit] instead of
9999+ an {!Emitter.t}, allowing them to work with any event sink. *)
100100+101101+val emit_yaml_node_fn : emitter:(Event.t -> unit) -> Yaml.t -> unit
102102+(** Emit a YAML node using an emitter function *)
103103+104104+val emit_yaml_fn :
105105+ emitter:(Event.t -> unit) ->
106106+ config:Emitter.config ->
107107+ Yaml.t -> unit
108108+(** Emit a complete YAML stream using an emitter function *)
109109+110110+val emit_value_node_fn :
111111+ emitter:(Event.t -> unit) ->
112112+ config:Emitter.config ->
113113+ Value.t -> unit
114114+(** Emit a Value node using an emitter function *)
115115+116116+val emit_value_fn :
117117+ emitter:(Event.t -> unit) ->
118118+ config:Emitter.config ->
119119+ Value.t -> unit
120120+(** Emit a complete Value stream using an emitter function *)
121121+122122+val emit_document_fn :
123123+ ?resolve_aliases:bool ->
124124+ emitter:(Event.t -> unit) ->
125125+ Document.t -> unit
126126+(** Emit a document using an emitter function *)
127127+128128+val emit_documents :
129129+ emitter:(Event.t -> unit) ->
130130+ config:Emitter.config ->
131131+ ?resolve_aliases:bool ->
132132+ Document.t list -> unit
133133+(** Emit multiple documents using an emitter function *)
+35
lib/span.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Source spans representing ranges in input *)
77+88+type t = {
99+ start : Position.t;
1010+ stop : Position.t;
1111+}
1212+1313+val make : start:Position.t -> stop:Position.t -> t
1414+(** Create a span from start and stop positions *)
1515+1616+val point : Position.t -> t
1717+(** Create a zero-width span at a single position *)
1818+1919+val merge : t -> t -> t
2020+(** Merge two spans into one covering both *)
2121+2222+val extend : t -> Position.t -> t
2323+(** Extend a span to a new stop position *)
2424+2525+val pp : Format.formatter -> t -> unit
2626+(** Pretty-print a span *)
2727+2828+val to_string : t -> string
2929+(** Convert span to string *)
3030+3131+val compare : t -> t -> int
3232+(** Compare two spans *)
3333+3434+val equal : t -> t -> bool
3535+(** Test equality of two spans *)
+54
lib/tag.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML tags for type information *)
77+88+type t = {
99+ handle : string; (** e.g., "!" or "!!" or "!foo!" *)
1010+ suffix : string; (** e.g., "str", "int", "custom/type" *)
1111+}
1212+1313+val make : handle:string -> suffix:string -> t
1414+(** Create a tag from handle and suffix *)
1515+1616+val of_string : string -> t option
1717+(** Parse a tag string *)
1818+1919+val to_string : t -> string
2020+(** Convert tag to string representation *)
2121+2222+val to_uri : t -> string
2323+(** Convert tag to full URI representation *)
2424+2525+val pp : Format.formatter -> t -> unit
2626+(** Pretty-print a tag *)
2727+2828+val equal : t -> t -> bool
2929+(** Test equality of two tags *)
3030+3131+val compare : t -> t -> int
3232+(** Compare two tags *)
3333+3434+(** {2 Standard Tags} *)
3535+3636+val null : t
3737+val bool : t
3838+val int : t
3939+val float : t
4040+val str : t
4141+val seq : t
4242+val map : t
4343+val binary : t
4444+val timestamp : t
4545+4646+(** {2 Tag Predicates} *)
4747+4848+val is_null : t -> bool
4949+val is_bool : t -> bool
5050+val is_int : t -> bool
5151+val is_float : t -> bool
5252+val is_str : t -> bool
5353+val is_seq : t -> bool
5454+val is_map : t -> bool
+43
lib/token.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** YAML token types produced by the scanner *)
77+88+type t =
99+ | Stream_start of Encoding.t
1010+ | Stream_end
1111+ | Version_directive of { major : int; minor : int }
1212+ | Tag_directive of { handle : string; prefix : string }
1313+ | Document_start (** --- *)
1414+ | Document_end (** ... *)
1515+ | Block_sequence_start
1616+ | Block_mapping_start
1717+ | Block_entry (** - *)
1818+ | Block_end (** implicit, from dedent *)
1919+ | Flow_sequence_start (** \[ *)
2020+ | Flow_sequence_end (** \] *)
2121+ | Flow_mapping_start (** \{ *)
2222+ | Flow_mapping_end (** \} *)
2323+ | Flow_entry (** , *)
2424+ | Key (** ? or implicit key *)
2525+ | Value (** : *)
2626+ | Anchor of string (** &name *)
2727+ | Alias of string (** *name *)
2828+ | Tag of { handle : string; suffix : string }
2929+ | Scalar of { style : Scalar_style.t; value : string }
3030+3131+type spanned = {
3232+ token : t;
3333+ span : Span.t;
3434+}
3535+3636+val pp_token : Format.formatter -> t -> unit
3737+(** Pretty-print a token *)
3838+3939+val pp : Format.formatter -> t -> unit
4040+(** Pretty-print a token (alias for pp_token) *)
4141+4242+val pp_spanned : Format.formatter -> spanned -> unit
4343+(** Pretty-print a spanned token *)
+70
lib/value.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** JSON-compatible YAML value representation *)
77+88+type t = [
99+ | `Null
1010+ | `Bool of bool
1111+ | `Float of float
1212+ | `String of string
1313+ | `A of t list
1414+ | `O of (string * t) list
1515+]
1616+1717+(** {2 Constructors} *)
1818+1919+val null : t
2020+val bool : bool -> t
2121+val int : int -> t
2222+val float : float -> t
2323+val string : string -> t
2424+val list : ('a -> t) -> 'a list -> t
2525+val obj : (string * t) list -> t
2626+2727+(** {2 Type Name} *)
2828+2929+val type_name : t -> string
3030+(** Get the type name for error messages *)
3131+3232+(** {2 Safe Accessors} *)
3333+3434+val as_null : t -> unit option
3535+val as_bool : t -> bool option
3636+val as_float : t -> float option
3737+val as_string : t -> string option
3838+val as_list : t -> t list option
3939+val as_assoc : t -> (string * t) list option
4040+val as_int : t -> int option
4141+4242+(** {2 Unsafe Accessors} *)
4343+4444+val to_null : t -> unit
4545+val to_bool : t -> bool
4646+val to_float : t -> float
4747+val to_string : t -> string
4848+val to_list : t -> t list
4949+val to_assoc : t -> (string * t) list
5050+val to_int : t -> int
5151+5252+(** {2 Object Access} *)
5353+5454+val mem : string -> t -> bool
5555+val find : string -> t -> t option
5656+val get : string -> t -> t
5757+val keys : t -> string list
5858+val values : t -> t list
5959+6060+(** {2 Combinators} *)
6161+6262+val combine : t -> t -> t
6363+val map : (t -> t) -> t -> t
6464+val filter : (t -> bool) -> t -> t
6565+6666+(** {2 Comparison} *)
6767+6868+val pp : Format.formatter -> t -> unit
6969+val equal : t -> t -> bool
7070+val compare : t -> t -> int
+3-9
lib/yaml.ml
···110110 match v with
111111 | `Scalar s ->
112112 (* Register anchor after we have the resolved node *)
113113- (match Scalar.anchor s with
114114- | Some name -> register_anchor name v
115115- | None -> ());
113113+ Option.iter (fun name -> register_anchor name v) (Scalar.anchor s);
116114 v
117115 | `Alias name ->
118116 expand_alias ~depth name
···126124 ~style:(Sequence.style seq)
127125 resolved_members) in
128126 (* Register anchor with resolved node *)
129129- (match Sequence.anchor seq with
130130- | Some name -> register_anchor name resolved
131131- | None -> ());
127127+ Option.iter (fun name -> register_anchor name resolved) (Sequence.anchor seq);
132128 resolved
133129 | `O map ->
134130 (* Process key-value pairs in document order *)
···144140 ~style:(Mapping.style map)
145141 resolved_pairs) in
146142 (* Register anchor with resolved node *)
147147- (match Mapping.anchor map with
148148- | Some name -> register_anchor name resolved
149149- | None -> ());
143143+ Option.iter (fun name -> register_anchor name resolved) (Mapping.anchor map);
150144 resolved
151145 in
152146 resolve ~depth:0 root
+63
lib/yaml.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Full YAML representation with anchors, tags, and aliases *)
77+88+type t = [
99+ | `Scalar of Scalar.t
1010+ | `Alias of string
1111+ | `A of t Sequence.t
1212+ | `O of (t, t) Mapping.t
1313+]
1414+1515+(** {2 Pretty Printing} *)
1616+1717+val pp : Format.formatter -> t -> unit
1818+1919+(** {2 Equality} *)
2020+2121+val equal : t -> t -> bool
2222+2323+(** {2 Conversion from Value} *)
2424+2525+val of_value : Value.t -> t
2626+(** Construct from JSON-compatible Value *)
2727+2828+(** {2 Alias Resolution} *)
2929+3030+val default_max_alias_nodes : int
3131+(** Default maximum nodes during alias expansion (10 million) *)
3232+3333+val default_max_alias_depth : int
3434+(** Default maximum alias nesting depth (100) *)
3535+3636+val resolve_aliases : ?max_nodes:int -> ?max_depth:int -> t -> t
3737+(** Resolve aliases by replacing them with referenced nodes.
3838+3939+ @param max_nodes Maximum number of nodes to create during expansion
4040+ @param max_depth Maximum depth of alias-within-alias resolution
4141+ @raise Error.Yamlrw_error if limits exceeded or undefined alias found *)
4242+4343+(** {2 Conversion to Value} *)
4444+4545+val to_value :
4646+ ?resolve_aliases_first:bool ->
4747+ ?max_nodes:int ->
4848+ ?max_depth:int ->
4949+ t -> Value.t
5050+(** Convert to JSON-compatible Value.
5151+5252+ @param resolve_aliases_first Whether to resolve aliases before conversion (default true)
5353+ @param max_nodes Maximum nodes during alias expansion
5454+ @param max_depth Maximum alias nesting depth
5555+ @raise Error.Yamlrw_error if unresolved aliases encountered *)
5656+5757+(** {2 Node Accessors} *)
5858+5959+val anchor : t -> string option
6060+(** Get anchor from any node *)
6161+6262+val tag : t -> string option
6363+(** Get tag from any node *)
+140
tests/test_yamlrw.ml
···343343 "resolve_aliases false", `Quick, test_resolve_aliases_false;
344344]
345345346346+(** Bug fix regression tests
347347+ These tests verify that issues fixed in ocaml-yaml don't occur in ocaml-yamlrw *)
348348+349349+(* Test for roundtrip of special string values (ocaml-yaml fix 225387d)
350350+ Strings like "true", "1.0", "null" etc. must be quoted on output so that
351351+ they round-trip correctly as strings, not as booleans/numbers/null *)
352352+let test_roundtrip_string_true () =
353353+ let original = `String "true" in
354354+ let emitted = to_string original in
355355+ let parsed = of_string emitted in
356356+ check_value "String 'true' roundtrips" original parsed
357357+358358+let test_roundtrip_string_false () =
359359+ let original = `String "false" in
360360+ let emitted = to_string original in
361361+ let parsed = of_string emitted in
362362+ check_value "String 'false' roundtrips" original parsed
363363+364364+let test_roundtrip_string_null () =
365365+ let original = `String "null" in
366366+ let emitted = to_string original in
367367+ let parsed = of_string emitted in
368368+ check_value "String 'null' roundtrips" original parsed
369369+370370+let test_roundtrip_string_number () =
371371+ let original = `String "1.0" in
372372+ let emitted = to_string original in
373373+ let parsed = of_string emitted in
374374+ check_value "String '1.0' roundtrips" original parsed
375375+376376+let test_roundtrip_string_integer () =
377377+ let original = `String "42" in
378378+ let emitted = to_string original in
379379+ let parsed = of_string emitted in
380380+ check_value "String '42' roundtrips" original parsed
381381+382382+let test_roundtrip_string_yes () =
383383+ let original = `String "yes" in
384384+ let emitted = to_string original in
385385+ let parsed = of_string emitted in
386386+ check_value "String 'yes' roundtrips" original parsed
387387+388388+let test_roundtrip_string_no () =
389389+ let original = `String "no" in
390390+ let emitted = to_string original in
391391+ let parsed = of_string emitted in
392392+ check_value "String 'no' roundtrips" original parsed
393393+394394+(* Test for integer display without decimal point (ocaml-yaml fix 999b1aa)
395395+ Float values that are integers should be emitted as "42" not "42." or "42.0" *)
396396+let test_emit_integer_float () =
397397+ let value = `Float 42.0 in
398398+ let result = to_string value in
399399+ (* Check the result doesn't contain "42." or "42.0" *)
400400+ Alcotest.(check bool) "no trailing dot"
401401+ true (not (String.length result >= 3 &&
402402+ result.[0] = '4' && result.[1] = '2' && result.[2] = '.'))
403403+404404+let test_emit_negative_integer_float () =
405405+ let value = `Float (-17.0) in
406406+ let result = to_string value in
407407+ let parsed = of_string result in
408408+ check_value "negative integer float roundtrips" value parsed
409409+410410+(* Test for special YAML floats: .nan, .inf, -.inf *)
411411+let test_parse_special_floats () =
412412+ let inf_result = of_string ".inf" in
413413+ (match inf_result with
414414+ | `Float f when Float.is_inf f && f > 0.0 -> ()
415415+ | _ -> Alcotest.fail "expected positive infinity");
416416+ let neg_inf_result = of_string "-.inf" in
417417+ (match neg_inf_result with
418418+ | `Float f when Float.is_inf f && f < 0.0 -> ()
419419+ | _ -> Alcotest.fail "expected negative infinity");
420420+ let nan_result = of_string ".nan" in
421421+ (match nan_result with
422422+ | `Float f when Float.is_nan f -> ()
423423+ | _ -> Alcotest.fail "expected NaN")
424424+425425+(* Test that bare "inf", "nan", "infinity" are NOT parsed as floats
426426+ (ocaml-yaml issue - OCaml's Float.of_string accepts these but YAML doesn't) *)
427427+let test_bare_inf_nan_are_strings () =
428428+ let inf_result = of_string "inf" in
429429+ (match inf_result with
430430+ | `String "inf" -> ()
431431+ | `Float _ -> Alcotest.fail "'inf' should be string, not float"
432432+ | _ -> Alcotest.fail "expected string 'inf'");
433433+ let nan_result = of_string "nan" in
434434+ (match nan_result with
435435+ | `String "nan" -> ()
436436+ | `Float _ -> Alcotest.fail "'nan' should be string, not float"
437437+ | _ -> Alcotest.fail "expected string 'nan'");
438438+ let infinity_result = of_string "infinity" in
439439+ (match infinity_result with
440440+ | `String "infinity" -> ()
441441+ | `Float _ -> Alcotest.fail "'infinity' should be string, not float"
442442+ | _ -> Alcotest.fail "expected string 'infinity'")
443443+444444+(* Test for quoted scalar preservation *)
445445+let test_quoted_scalar_preserved () =
446446+ (* When a scalar is quoted, it should be preserved as a string even if
447447+ it looks like a number/boolean *)
448448+ check_value "double-quoted true is string"
449449+ (`String "true") (of_string {|"true"|});
450450+ check_value "single-quoted 42 is string"
451451+ (`String "42") (of_string "'42'");
452452+ check_value "double-quoted null is string"
453453+ (`String "null") (of_string {|"null"|})
454454+455455+(* Test complex roundtrip with mixed types *)
456456+let test_complex_roundtrip () =
457457+ let original = `O [
458458+ ("string_true", `String "true");
459459+ ("bool_true", `Bool true);
460460+ ("string_42", `String "42");
461461+ ("int_42", `Float 42.0);
462462+ ("string_null", `String "null");
463463+ ("actual_null", `Null);
464464+ ] in
465465+ let emitted = to_string original in
466466+ let parsed = of_string emitted in
467467+ check_value "complex roundtrip preserves types" original parsed
468468+469469+let bugfix_regression_tests = [
470470+ "roundtrip string 'true'", `Quick, test_roundtrip_string_true;
471471+ "roundtrip string 'false'", `Quick, test_roundtrip_string_false;
472472+ "roundtrip string 'null'", `Quick, test_roundtrip_string_null;
473473+ "roundtrip string '1.0'", `Quick, test_roundtrip_string_number;
474474+ "roundtrip string '42'", `Quick, test_roundtrip_string_integer;
475475+ "roundtrip string 'yes'", `Quick, test_roundtrip_string_yes;
476476+ "roundtrip string 'no'", `Quick, test_roundtrip_string_no;
477477+ "emit integer float without decimal", `Quick, test_emit_integer_float;
478478+ "emit negative integer float", `Quick, test_emit_negative_integer_float;
479479+ "parse special floats (.inf, -.inf, .nan)", `Quick, test_parse_special_floats;
480480+ "bare inf/nan/infinity are strings", `Quick, test_bare_inf_nan_are_strings;
481481+ "quoted scalars preserved as strings", `Quick, test_quoted_scalar_preserved;
482482+ "complex roundtrip preserves types", `Quick, test_complex_roundtrip;
483483+]
484484+346485(** Run all tests *)
347486348487let () =
···355494 "multiline", multiline_tests;
356495 "errors", error_tests;
357496 "alias_limits", alias_limit_tests;
497497+ "bugfix_regression", bugfix_regression_tests;
358498 ]