From 36daf47dde9b0c16dacef31163ea74effdd5e7f3 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 18 Aug 2026 13:55:18 +0200 Subject: feat(overlay): a flat INI front end, which verifies its own output A convenience format for calendars that add a few local feasts and drop one or two universal entries. Section names are slugs, a [overlay] section carries the id, and status/subject/layer default so the common case -- an ordinary local saint's feast -- says only what distinguishes it. It is a FRONT DOOR, not a second data model. It parses to exactly the Overlay.t the S-expression form parses to, and everything downstream is the same code on the same values; a test asserts an INI overlay and its hand-written sexp equivalent produce identical Overlay.t values. It is also deliberately less expressive -- Add, Suppress and single-field Edit only -- and refuses Replace, multi-field edits and citation edits BY NAME rather than dropping them silently. Anything it cannot say is a reason to write sexp. Little of this is new machinery: tools/bootstrap_sanctoral.ml has parsed INI and mapped it to celebrations since the sanctoral was bootstrapped from lectio. The dates needed extending, since that mapping handled only MM-DD; the flat forms are easter+N/easter-N and mon/day/nth, with nth negative to count from the end. `colitur convert` is a separate step rather than --overlay sniffing the extension, so the author can read what their INI became. When a date form was mistyped, "what did the engine actually get" is the question, and an invisible transpile cannot answer it. The conversion verifies its own output: the emitted text is parsed back with the same function that loads an overlay and must equal what the INI denoted, or nothing is written. That is the point of the module. A transpiler emitting valid-but-wrong sexp is the failure a convenience format invites, and `colitur check` could never catch it -- the output would parse cleanly and mean something else. That check was WRONG on the first attempt, in exactly the way it exists to prevent. It re-serialised the parsed value instead of parsing the text being returned, so it verified t -> sexp -> t, which is true by construction and proves nothing. Found by mutation: corrupting the renderer to emit a different overlay id sailed through and exited 0. It now parses the returned text, the mutation is caught with exit 2, and two tests fail under it where none did before. --- lib/kernel/overlay_ini.mli | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 lib/kernel/overlay_ini.mli (limited to 'lib/kernel/overlay_ini.mli') diff --git a/lib/kernel/overlay_ini.mli b/lib/kernel/overlay_ini.mli new file mode 100644 index 0000000..bace93b --- /dev/null +++ b/lib/kernel/overlay_ini.mli @@ -0,0 +1,69 @@ +(** A flat INI front end for overlay files. + + This is a CONVENIENCE FORMAT, not a second data model. It parses to exactly + the {!Overlay.t} the S-expression form parses to, and everything downstream + -- merge, diagnostics, validation -- is the same code on the same values. + There is deliberately no second semantics to keep in step. + + It is also deliberately LESS EXPRESSIVE than the sexp form. It covers [Add], + [Suppress] and single-field [Edit], which is what a diocesan or parish + calendar needs; [Replace], multi-field edits and citation edits are not + expressible and the parser says so by name rather than failing obscurely. + Anything it cannot say is a reason to write sexp, not a reason to grow this. + + {1 Format} + + Section names are slugs. A [\[overlay\]] section carries the file's id. + + {v + [overlay] + id = my-parish + + [our-patron] + date = 07-11 + rank = class-3 + colour = white + name.en = St Example, Patron + + [some-universal-slug] + suppress = yes + v} + + Dates take three forms, matching {!Date_spec}: [MM-DD], [easter+N] or + [easter-N], and [mon/day/nth] such as [oct/sun/1] or [oct/sun/-1]. *) + +(** [parse ~rank_of_string text] is the overlay [text] denotes. + + [rank_of_string] is supplied by the rite, exactly as [Overlay.load] takes + [rank_of_sexp]: the kernel does not know one rite's rank vocabulary from + another's. + + Errors carry the section name and the offending value, never a source-file + path -- this format exists for people who are not reading the source. *) +val parse : + rank_of_string:(string -> 'r option) -> string -> ('r Overlay.t, string) result + +(** [to_sexp_string t] renders [t] as the S-expression form, with a header + noting that it was generated. *) +val to_sexp_string : ('r -> Sexplib0.Sexp.t) -> 'r Overlay.t -> string + +(** [convert ~rank_of_string ~rank_to_sexp ~rank_of_sexp text] parses [text], + renders it, and PROVES the rendering before returning it: the emitted text + is parsed back with the very function the engine uses to load an overlay, + and the result must equal what the INI denoted. + + That check is the point of this module. A transpiler that emits + syntactically valid but semantically wrong output is the failure mode a + convenience format invites, and it is one [colitur check] could not catch, + since the emitted file would parse cleanly and simply mean something else. + Verifying the round trip here makes that class of bug impossible to ship + rather than merely unlikely. + + [Error] on a parse failure, and on a round-trip mismatch -- which is a bug + in this module, and says so. *) +val convert : + rank_of_string:(string -> 'r option) -> + rank_to_sexp:('r -> Sexplib0.Sexp.t) -> + rank_of_sexp:(Sexplib0.Sexp.t -> 'r) -> + string -> + (string, string) result -- cgit v1.3